{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input",
  "type": "registry:ui",
  "title": "Input",
  "description": "Text field with prefix and suffix slots, a clear that fades in, and Field-aware invalid state.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/input.tsx",
      "content": "\"use client\";\nimport { Input as BaseInput } from \"@base-ui/react/input\";\nimport { useCallback, useRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { X } from \"@/lib/icons\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nexport type InputProps = Omit<React.ComponentProps<\"input\">, \"size\" | \"prefix\" | \"value\" | \"defaultValue\"> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Leading content: an icon, a unit like \"$\" or a fixed part like \"https://\". Strings are set as quiet text. */\n  prefix?: React.ReactNode;\n  /** Trailing content: a unit, a status icon or an `InputKbd` hint. */\n  suffix?: React.ReactNode;\n  /** Shows a clear button while there is a value. Clearing refocuses the field. */\n  clearable?: boolean;\n  /** Called after the clear button empties the field. */\n  onClear?: () => void;\n  /** Marks the field invalid. Also picked up automatically inside a Base UI Field. */\n  invalid?: boolean;\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Classes for the inner <input>; `className` styles the outer box. */\n  inputClassName?: string;\n};\n\n// The box carries the border and focus halo so prefix, text, clear and suffix\n// read as one control. Invalid state comes from the prop or from Field's data-invalid.\nconst box = cn(\n  \"group/input relative flex w-full min-w-0 items-center border bg-raised text-fg shadow-[var(--shadow)]\",\n  \"border-line-2 transition-[border-color,box-shadow,background-color] duration-150 ease-out\",\n  \"hover:border-fg-4\",\n  \"focus-within:border-fg-3 focus-within:ring-3 focus-within:ring-fg/8 hover:focus-within:border-fg-3\",\n  \"data-invalid:border-danger/70 data-invalid:hover:border-danger data-invalid:focus-within:border-danger data-invalid:focus-within:ring-danger/15\",\n  \"has-[input[data-invalid]]:border-danger/70 has-[input[data-invalid]]:focus-within:border-danger has-[input[data-invalid]]:focus-within:ring-danger/15\",\n  \"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:shadow-none data-disabled:hover:border-line-2\",\n  \"data-readonly:bg-frame data-readonly:shadow-none data-readonly:hover:border-line-2 data-readonly:focus-within:border-line-2\",\n);\n\nconst sizes = {\n  sm: { box: \"h-7 gap-1.5 rounded-md px-2 text-base sm:text-[12.5px]\", icon: \"[&_svg]:size-3.5\" },\n  md: { box: \"h-8 gap-2 rounded-lg px-2.5 text-base sm:text-[13px]\", icon: \"[&_svg]:size-4\" },\n  lg: { box: \"h-9 gap-2 rounded-lg px-3 text-base sm:text-[13px]\", icon: \"[&_svg]:size-4\" },\n};\n\nexport function Input({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  prefix,\n  suffix,\n  clearable = false,\n  onClear,\n  invalid,\n  size = \"md\",\n  disabled,\n  readOnly,\n  className,\n  inputClassName,\n  ref,\n  ...rest\n}: InputProps) {\n  const [value, setValue] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });\n  const inner = useRef<HTMLInputElement | null>(null);\n  const setRef = useCallback(\n    (node: HTMLInputElement | null) => {\n      inner.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n\n  const showClear = clearable && value.length > 0 && !disabled && !readOnly;\n  const s = sizes[size];\n\n  return (\n    <div\n      data-slot=\"input\"\n      data-size={size}\n      data-invalid={invalid || undefined}\n      data-disabled={disabled || undefined}\n      data-readonly={readOnly || undefined}\n      className={cn(box, s.box, className)}\n      // Pressing the padding, the prefix or the suffix puts the caret in the field\n      // instead of doing nothing, the way a single native control would.\n      onMouseDown={(e) => {\n        const t = e.target as HTMLElement;\n        if (t.closest(\"input, button, a\") || disabled) return;\n        e.preventDefault();\n        inner.current?.focus();\n      }}\n    >\n      {prefix != null && <Affix side=\"prefix\" className={s.icon}>{prefix}</Affix>}\n\n      <BaseInput\n        ref={setRef}\n        value={value}\n        onValueChange={(next) => setValue(next)}\n        disabled={disabled}\n        readOnly={readOnly}\n        aria-invalid={invalid || undefined}\n        className={cn(\n          \"h-full min-w-0 flex-1 bg-transparent text-inherit outline-none placeholder:text-fg-4\",\n          \"disabled:cursor-not-allowed read-only:cursor-default\",\n          // The browser's autofill paints its own yellow or blue; keep the surface and the ink ours.\n          \"autofill:shadow-[inset_0_0_0_1000px_var(--raised)] autofill:[-webkit-text-fill-color:var(--fg)]\",\n          \"[&::-webkit-search-cancel-button]:appearance-none\",\n          inputClassName,\n        )}\n        {...rest}\n      />\n\n      {clearable && (\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          aria-label=\"Clear\"\n          inert={!showClear}\n          data-state={showClear ? \"visible\" : \"hidden\"}\n          onClick={() => {\n            setValue(\"\");\n            onClear?.();\n            inner.current?.focus();\n          }}\n          className={cn(\n            \"relative -mr-1 grid size-5 shrink-0 place-items-center rounded-[5px] text-fg-3 outline-none\",\n            \"before:absolute before:-inset-1.5 before:content-[''] pointer-coarse:before:-inset-3\",\n            \"hover:bg-hover hover:text-fg active:scale-[0.88]\",\n            // Arrives on the expo curve, leaves faster and simpler.\n            \"transition-[opacity,scale,filter,background-color,color] duration-100 ease-out\",\n            // At rest it stays out of the way; it shows while the field is hovered or in use.\n            \"pointer-events-none scale-75 opacity-0 blur-[2px] motion-reduce:scale-100 motion-reduce:blur-none\",\n            \"data-[state=visible]:group-hover/input:pointer-events-auto data-[state=visible]:group-hover/input:scale-100 data-[state=visible]:group-hover/input:opacity-100 data-[state=visible]:group-hover/input:blur-none\",\n            \"data-[state=visible]:group-focus-within/input:pointer-events-auto data-[state=visible]:group-focus-within/input:scale-100 data-[state=visible]:group-focus-within/input:opacity-100 data-[state=visible]:group-focus-within/input:blur-none\",\n            \"data-[state=visible]:duration-200 data-[state=visible]:ease-out-expo\",\n            size === \"sm\" && \"size-4.5\",\n          )}\n        >\n          <X size={size === \"sm\" ? 12 : 14} />\n        </button>\n      )}\n\n      {suffix != null && <Affix side=\"suffix\" className={s.icon}>{suffix}</Affix>}\n    </div>\n  );\n}\n\nfunction Affix({ side, className, children }: { side: \"prefix\" | \"suffix\"; className?: string; children: React.ReactNode }) {\n  const text = typeof children === \"string\" || typeof children === \"number\";\n  return (\n    <span\n      data-slot={side}\n      className={cn(\n        \"flex shrink-0 select-none items-center text-fg-3 transition-colors duration-150\",\n        // Icons come up one step while the field is in use, so it reads as live.\n        \"group-focus-within/input:text-fg-2\",\n        // Text sits close to the value so \"stealth.pm/\" + \"northwind\" reads as one address.\n        text && (side === \"prefix\" ? \"-mr-1.5 whitespace-nowrap tabular group-data-[size=sm]/input:-mr-1\" : \"whitespace-nowrap tabular\"),\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport type InputKbdProps = React.ComponentProps<\"kbd\">;\n\n/** A keycap for shortcut hints in the suffix, e.g. <InputKbd>⌘K</InputKbd>. */\nexport function InputKbd({ className, ...rest }: InputKbdProps) {\n  return (\n    <kbd\n      className={cn(\n        \"inline-flex h-5 min-w-5 items-center justify-center rounded-[5px] border border-line-2 bg-frame px-1 font-mono text-[10.5px] font-normal leading-none text-fg-3\",\n        className,\n      )}\n      {...rest}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/input"
}
