{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input",
  "title": "File Input",
  "description": "Styled file picker: hidden native input under a custom surface with a leading icon (lucide or remixicon), placeholder or selected file name, multiple-file count and an optional clear button. Follows the Input contract: 3 variants (outline, soft, ghost), 3 sizes (sm, md, lg), 3 tones (neutral, success, danger). States: empty, selected, disabled, invalid via aria-invalid.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/ui/file-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Paperclip, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst fileInputVariants = cva(\n  \"flex w-full min-w-0 cursor-pointer items-center outline-none transition-[color,box-shadow] duration-(--motion-fast) has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[3px] has-[input:focus-visible]:ring-ring/50 has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 has-[input[aria-invalid=true]]:border-danger [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\",\n  {\n    variants: {\n      variant: {\n        outline: \"border border-field-border bg-transparent shadow-xs dark:bg-input/30\",\n        soft: \"border border-transparent bg-surface-3 dark:bg-input/50\",\n        ghost: \"border border-transparent bg-transparent hover:bg-surface-3\",\n      },\n      size: {\n        sm: \"min-h-8 gap-1.5 rounded-md px-2.5 py-1.5 text-sm\",\n        md: \"min-h-9 gap-2 rounded-lg px-3 py-1.5 text-sm\",\n        lg: \"min-h-10 gap-2 rounded-lg px-3.5 py-2 text-base md:text-sm\",\n      },\n      tone: {\n        neutral: \"\",\n        success:\n          \"border-success has-[input:focus-visible]:border-success has-[input:focus-visible]:ring-success/20 dark:has-[input:focus-visible]:ring-success/40\",\n        danger:\n          \"border-danger has-[input:focus-visible]:border-danger has-[input:focus-visible]:ring-danger/20 dark:has-[input:focus-visible]:ring-danger/40\",\n      },\n    },\n    defaultVariants: { variant: \"outline\", size: \"md\", tone: \"neutral\" },\n  }\n)\n\ninterface FileInputProps\n  extends Omit<React.ComponentProps<\"input\">, \"size\" | \"type\">,\n    VariantProps<typeof fileInputVariants> {\n  /** Text shown before a file is picked. */\n  placeholder?: string\n  /** Leading icon; defaults to a lucide Paperclip. Accepts remixicon `<i>` too. */\n  icon?: React.ReactNode\n  /** Show a clear button once a file is selected. */\n  clearable?: boolean\n}\n\nfunction FileInput({\n  className,\n  variant,\n  size,\n  tone,\n  placeholder = \"Choose a file\",\n  icon,\n  clearable = true,\n  onChange,\n  ...props\n}: FileInputProps) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [names, setNames] = React.useState<string[]>([])\n\n  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    setNames(Array.from(event.target.files ?? []).map((f) => f.name))\n    onChange?.(event)\n  }\n\n  const clear = (event: React.MouseEvent<HTMLButtonElement>) => {\n    // A button inside a label re-triggers the file dialog unless defaulted out.\n    event.preventDefault()\n    event.stopPropagation()\n    const input = inputRef.current\n    if (!input) return\n    input.value = \"\"\n    setNames([])\n    input.dispatchEvent(new Event(\"change\", { bubbles: true }))\n  }\n\n  const selected = names.length > 0\n  const text = selected\n    ? names.length > 1\n      ? `${names.length} files`\n      : names[0]\n    : placeholder\n\n  return (\n    <label\n      data-slot=\"file-input\"\n      data-variant={variant ?? \"outline\"}\n      data-tone={tone ?? \"neutral\"}\n      data-state={selected ? \"selected\" : \"empty\"}\n      className={cn(fileInputVariants({ variant, size, tone, className }))}\n    >\n      <input\n        ref={inputRef}\n        type=\"file\"\n        data-slot=\"file-input-control\"\n        className=\"sr-only\"\n        onChange={handleChange}\n        {...props}\n      />\n      <span\n        data-slot=\"file-input-icon\"\n        aria-hidden=\"true\"\n        className=\"flex shrink-0 items-center justify-center text-muted-foreground\"\n      >\n        {icon ?? <Paperclip />}\n      </span>\n      <span\n        data-slot=\"file-input-text\"\n        className={cn(\"flex-1 [overflow-wrap:anywhere]\", !selected && \"text-muted-foreground\")}\n      >\n        {text}\n      </span>\n      {clearable && selected && (\n        <button\n          type=\"button\"\n          data-slot=\"file-input-clear\"\n          aria-label=\"Clear selected file\"\n          onClick={clear}\n          className=\"relative flex shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors duration-(--motion-fast) after:absolute after:-inset-2 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n        >\n          <X />\n        </button>\n      )}\n    </label>\n  )\n}\n\nexport { FileInput, fileInputVariants, type FileInputProps }\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Use in place of a bare input[type=file]. The icon prop swaps the paperclip; clearable={false} hides the clear button. Reads files from the native change event, so existing form handling keeps working.",
  "categories": [
    "form"
  ],
  "type": "registry:ui"
}