{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input",
  "title": "Input",
  "description": "Input from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/input.tsx",
      "content": "'use client';\n\nimport { Eye, EyeOff, Loader2, Search } from 'lucide-react';\nimport * as React from 'react';\n\nimport { fieldHaloZero, focusRingField, invalidRing, transitionBase } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// Field heights: `xs` is the compact `--size-control-xs` height (28, 24 in zero), `sm`, `md` and `lg` follow the `--size-control-*` tokens (compact in zero). No size keeps the legacy field, and in the zero preset the `md` field.\nexport type InputSize = 'xs' | 'sm' | 'md' | 'lg';\n\nconst SIZE_CLASS: Record<InputSize, string> = {\n  xs: 'h-control-xs px-control-px-sm py-0',\n  sm: 'h-control-sm px-control-px-sm py-0',\n  md: 'h-control-md px-control-px-sm py-0',\n  lg: 'h-control-lg px-control-px-sm py-0',\n};\n\nexport interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {\n  // Field height, see `InputSize`. Overrides `compact`. A number is the native `size` attribute (visible characters) and keeps the field height.\n  size?: InputSize | number;\n  clamp?: boolean;\n  // Dense 32px field for inline rows (tables, toolbars). Default keeps the 40px field.\n  compact?: boolean;\n  /** `default` is the plain field; `search` wraps it with a decorative leading icon (`className` lands on the wrapper, `inputClassName` on the field). */\n  variant?: 'default' | 'search';\n  // Extra classes for the inner field. In the search variant `className` styles the wrapper instead.\n  inputClassName?: string;\n  // `readonly` sets the native readOnly attribute and the muted Figma read-only surface. Default keeps the plain look.\n  appearance?: 'default' | 'readonly';\n  // Trailing spinner while a value is validated or loaded (sets aria-busy).\n  loading?: boolean;\n  // Success border (Figma Color=Success). Invalid still wins via aria-invalid.\n  success?: boolean;\n  leadingIcon?: React.ReactNode;\n  trailingIcon?: React.ReactNode;\n  // Attached blocks before/after the field: text such as \"https://\", a Select, or a Button.\n  leadingAddon?: React.ReactNode;\n  trailingAddon?: React.ReactNode;\n  // Password only: adds an eye toggle (aria-pressed) that switches the field between masked and plain text.\n  revealable?: boolean;\n  // Accessible names of the reveal toggle. Win over the provider text.\n  showPasswordLabel?: string;\n  hidePasswordLabel?: string;\n  // Interactive trailing slot sized for a button (copy, send). Use an icon Button with size control-sm.\n  trailingAction?: React.ReactNode;\n}\n\nconst iconPadLeft = 'pl-[calc(theme(spacing.4)+theme(spacing.icon-xs)+theme(spacing.gap-control))]';\nconst iconPadRight = 'pr-[calc(theme(spacing.4)+theme(spacing.icon-xs)+theme(spacing.gap-control))]';\n// Same room when the field padding is the control token (sized fields and the zero preset).\nconst iconPadLeftSized = 'pl-[calc(theme(spacing.control-px-sm)+theme(spacing.icon-xs)+theme(spacing.gap-control))]';\nconst iconPadRightSized = 'pr-[calc(theme(spacing.control-px-sm)+theme(spacing.icon-xs)+theme(spacing.gap-control))]';\n// Right padding for 0 to 2 control-sm sized actions, with or without a trailing icon before them.\nconst actionPad = {\n  '1': 'pr-[calc(theme(spacing.control-sm)+theme(spacing.2))]',\n  '2': 'pr-[calc(theme(spacing.control-sm)*2+theme(spacing.2))]',\n  '1i': 'pr-[calc(theme(spacing.control-sm)+theme(spacing.2)+theme(spacing.icon-xs)+theme(spacing.gap-control))]',\n  '2i': 'pr-[calc(theme(spacing.control-sm)*2+theme(spacing.2)+theme(spacing.icon-xs)+theme(spacing.gap-control))]',\n} as const;\nconst addonBase =\n  'flex shrink-0 items-center border border-solid border-input bg-muted px-3 typography-input-text text-text-muted';\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  (\n    {\n      className,\n      inputClassName,\n      type,\n      clamp = false,\n      compact = false,\n      size: sizeProp,\n      variant = 'default',\n      appearance = 'default',\n      loading = false,\n      success = false,\n      leadingIcon,\n      trailingIcon,\n      leadingAddon,\n      trailingAddon,\n      readOnly,\n      revealable = false,\n      showPasswordLabel,\n      hidePasswordLabel,\n      trailingAction,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels({ showPassword: showPasswordLabel, hidePassword: hidePasswordLabel });\n    const zero = usePreset() === 'zero';\n    // Zero: padding follows the control padding token (10) and the field is the md height unless a size is given.\n    const size = typeof sizeProp === 'string' ? sizeProp : undefined;\n    const effectiveSize = size ?? (zero ? (compact ? 'sm' : 'md') : undefined);\n    const [revealed, setRevealed] = React.useState(false);\n    const canReveal = revealable && type === 'password';\n    const effectiveType = canReveal && revealed ? 'text' : type;\n    const actionCount = (canReveal ? 1 : 0) + (trailingAction ? 1 : 0);\n    const readonlyLook = appearance === 'readonly';\n    const trailing = loading ? (\n      <Loader2 aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs animate-spin text-icon-subtle motion-reduce:animate-none\" />\n    ) : (\n      trailingIcon\n    );\n    const hasActions = actionCount > 0;\n    const decorated = Boolean(leadingIcon || trailing || leadingAddon || trailingAddon || hasActions);\n    const field = (\n      <input\n        type={effectiveType}\n        size={typeof sizeProp === 'number' ? sizeProp : undefined}\n        className={cn(\n          'flex',\n          'w-full',\n          'rounded-control',\n          'border',\n          'border-solid',\n          'border-input',\n          'px-4',\n          'py-2',\n          'bg-background',\n          'text-foreground',\n          'file:border-0',\n          'file:bg-transparent',\n          'file:text-sm',\n          'file:font-medium',\n          'placeholder:text-input-foreground-placeholder',\n          'typography-input-text',\n          // Below sm the field text is at least 16px so iOS Safari does not zoom on focus.\n          'max-sm:text-[max(16px,var(--typo-input-text-size,1rem))]',\n          'leading-normal',\n          transitionBase,\n          // Hover: darker border + soft field shadow (no resting shadow, no background tint).\n          'hover:border-border-bold',\n          'hover:shadow-field-hover',\n          !zero && focusRingField,\n          // Invalid state driven by `aria-invalid` (forwarded by <FormControl> or set directly).\n          invalidRing,\n          'disabled:cursor-not-allowed',\n          'disabled:opacity-disabled',\n          { 'text-clamp': clamp },\n          { 'leading-clamp': clamp },\n          zero && fieldHaloZero,\n          compact && !effectiveSize && 'h-8 px-3 py-1',\n          effectiveSize && SIZE_CLASS[effectiveSize],\n          readonlyLook && 'bg-muted hover:border-input hover:shadow-none',\n          success && 'border-border-success',\n          leadingIcon && variant !== 'search' && (effectiveSize ? iconPadLeftSized : iconPadLeft),\n          trailing && !hasActions && (effectiveSize ? iconPadRightSized : iconPadRight),\n          hasActions && actionPad[`${actionCount as 1 | 2}${trailing ? 'i' : ''}` as keyof typeof actionPad],\n          leadingAddon && 'rounded-l-none',\n          trailingAddon && 'rounded-r-none',\n          // Room for the leading icon: field padding + icon + control gap, all from tokens.\n          variant === 'search' && (effectiveSize ? iconPadLeftSized : iconPadLeft),\n          variant === 'search' ? undefined : className,\n          inputClassName,\n        )}\n        ref={ref}\n        readOnly={readonlyLook ? true : readOnly}\n        aria-busy={loading || undefined}\n        {...props}\n      />\n    );\n\n    if (decorated && variant !== 'search') {\n      return (\n        <div className=\"flex w-full items-stretch\" data-input-group=\"\">\n          {leadingAddon ? <span className={cn(addonBase, 'rounded-l-control border-r-0')}>{leadingAddon}</span> : null}\n          <div className=\"relative min-w-0 flex-1\">\n            {leadingIcon ? (\n              <span aria-hidden=\"true\" className={cn('pointer-events-none absolute top-1/2 flex h-icon-xs w-icon-xs -translate-y-1/2 items-center justify-center text-icon-subtle', effectiveSize ? 'left-control-px-sm' : 'left-4')}>\n                {leadingIcon}\n              </span>\n            ) : null}\n            {field}\n            {hasActions ? (\n              <span className=\"absolute inset-y-0 right-1 flex items-center gap-1\">\n                {trailing ? (\n                  <span className=\"flex h-icon-xs w-icon-xs items-center justify-center text-icon-subtle\">{trailing}</span>\n                ) : null}\n                {canReveal ? (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"control-sm\"\n                    className=\"w-control-sm px-0\"\n                    aria-pressed={revealed}\n                    aria-label={revealed ? l.hidePassword : l.showPassword}\n                    disabled={props.disabled}\n                    onClick={() => setRevealed((value) => !value)}\n                  >\n                    {revealed ? <EyeOff aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs\" /> : <Eye aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs\" />}\n                  </Button>\n                ) : null}\n                {trailingAction}\n              </span>\n            ) : trailing ? (\n              <span className={cn('absolute top-1/2 flex h-icon-xs w-icon-xs -translate-y-1/2 items-center justify-center text-icon-subtle', effectiveSize ? 'right-control-px-sm' : 'right-4')}>{trailing}</span>\n            ) : null}\n          </div>\n          {trailingAddon ? <span className={cn(addonBase, 'rounded-r-control border-l-0')}>{trailingAddon}</span> : null}\n        </div>\n      );\n    }\n\n    if (variant !== 'search') return field;\n\n    return (\n      <div className={cn('relative w-full', className)} data-input-search=\"\">\n        <span\n          aria-hidden=\"true\"\n          className={cn('pointer-events-none absolute top-1/2 flex h-icon-xs w-icon-xs -translate-y-1/2 items-center justify-center text-icon-subtle', effectiveSize ? 'left-control-px-sm' : 'left-4')}\n        >\n          {leadingIcon ?? <Search className=\"h-icon-xs w-icon-xs\" />}\n        </span>\n        {field}\n      </div>\n    );\n  },\n);\nInput.displayName = 'Input';\n\nexport type InputHintProps = React.HTMLAttributes<HTMLParagraphElement> & {\n  /** Switches to the error hint colour. Pair with `aria-invalid` on the field and link via `aria-describedby`. */\n  invalid?: boolean;\n  // Hint tone: `warning` uses the warning foreground. `invalid` still wins.\n  tone?: 'default' | 'warning';\n};\n\n// Helper text under a field in the `input-hint` type role (themeable size/weight).\nconst InputHint = React.forwardRef<HTMLParagraphElement, InputHintProps>(\n  ({ className, invalid = false, tone = 'default', ...props }, ref) => (\n    <p\n      ref={ref}\n      className={cn(\n        'typography-input-hint',\n        invalid ? 'text-input-foreground-hint-error' : tone === 'warning' ? 'text-text-warning' : 'text-input-foreground-hint',\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nInputHint.displayName = 'InputHint';\n\nexport { Input, InputHint };\n\n// --- KNOWN TOKEN GAP (component-library-maintainer cannot close this from Input.tsx — it is a\n// color VALUE owned by the design tokens; Input only consumes tokens by name and\n// must not hardcode a raw hex/palette value around it) ---\n//\n// Selected-text highlight (`::selection`, i.e. the state the browser gives text the user has\n// dragged over inside the field) has no dedicated token today — no `selection-background` /\n// `selection-foreground` semantic role exists in the token repo, so a `selection:bg-…` utility\n// would either fall back to the browser default or require a raw palette value, which is\n// disallowed. Leaving the browser default un-styled until that token pair exists; add\n// `selection:bg-selection-background selection:text-selection-foreground` here once it ships.\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}