{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input-compact",
  "title": "Input-compact file",
  "description": "Styled file: the Input-compact set. 5 decorative file variations (InlineFile, MinimalFile, NarrowFile, RowFile, BareFile) on the ai2 token system, driven by CSS token transitions and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/file-input-compact.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Paperclip, Plus, Upload, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Compact file-input family: 5 dense pickers that fit into tight places. They all\n   stay on a single line and do not disturb the vertical rhythm; they are meant to\n   be used inside form rows, table cells and toolbars. Each export wraps a real\n   <input type=\"file\">; the trigger is focusable and works with the keyboard.\n   Because the clear button is under 24px it carries an invisible touch-area\n   extension (relative after:absolute after:-inset-1.5). Color comes ONLY from\n   semantic tokens, with transparency via color-mix. Deterministic, no animation\n   (only token transitions), so it is identical under reduced motion. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype FileInputProps = {\n  className?: string\n  size?: StyledSize\n  multiple?: boolean\n  accept?: string\n  onFilesChange?: (files: File[]) => void\n}\n\nfunction fileKey(file: File): string {\n  return `${file.name}:${file.size}:${file.lastModified}`\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`\n  return `${Math.round(bytes / (1024 * 1024))} MB`\n}\n\nfunction useFiles(multiple: boolean | undefined, onFilesChange?: (files: File[]) => void) {\n  const [files, setFiles] = React.useState<File[]>([])\n\n  const add = React.useCallback(\n    (incoming: FileList | File[] | null) => {\n      if (!incoming) return\n      const list = Array.from(incoming)\n      if (list.length === 0) return\n      setFiles((prev) => {\n        if (!multiple) {\n          const next = [list[0]]\n          onFilesChange?.(next)\n          return next\n        }\n        const map = new Map(prev.map((f) => [fileKey(f), f]))\n        for (const f of list) map.set(fileKey(f), f)\n        const next = Array.from(map.values())\n        onFilesChange?.(next)\n        return next\n      })\n    },\n    [multiple, onFilesChange]\n  )\n\n  const remove = React.useCallback(\n    (key: string) => {\n      setFiles((prev) => {\n        const next = prev.filter((f) => fileKey(f) !== key)\n        onFilesChange?.(next)\n        return next\n      })\n    },\n    [onFilesChange]\n  )\n\n  const clear = React.useCallback(() => {\n    setFiles([])\n    onFilesChange?.([])\n  }, [onFilesChange])\n\n  return { files, add, remove, clear }\n}\n\n/* The control height is aligned to the Button scale: sm=h-7, md=h-8, lg=h-9,\n   xl=h-10. The Compact family deliberately sits one step shorter. */\nconst controlSize: Record<StyledSize, string> = {\n  sm: \"h-7 gap-1.5 rounded-md px-2 text-xs\",\n  md: \"h-8 gap-1.5 rounded-md px-2.5 text-sm\",\n  lg: \"h-9 gap-2 rounded-lg px-3 text-sm\",\n  xl: \"h-10 gap-2 rounded-lg px-3.5 text-sm\",\n}\n\nconst textSize: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-sm\",\n  xl: \"text-sm\",\n}\n\nconst clearBtn =\n  \"relative shrink-0 rounded-sm text-muted-foreground outline-none transition-colors duration-(--motion-fast) hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:-inset-1.5 motion-reduce:transition-none\"\n\nconst iconCompat = \"[&>svg]:size-3.5 [&>svg]:shrink-0 [&>i]:text-sm [&>i]:leading-none\"\n\nfunction label(files: File[], empty: string): string {\n  if (files.length === 0) return empty\n  if (files.length === 1) return files[0].name\n  return `${files.length} files`\n}\n\n/* 1) InlineFile: metin akisi icinde duran, alt cizgili baglanti gibi tetikleyici. */\nexport function InlineFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, clear } = useFiles(multiple, onFilesChange)\n\n  return (\n    <span data-slot=\"styled-file-input\" className={cn(\"inline-flex items-center gap-1.5\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose a file\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"inline-flex items-center gap-1 rounded-sm font-medium text-primary underline underline-offset-4 outline-none transition-colors duration-(--motion-fast) hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n          iconCompat,\n          textSize[size]\n        )}\n      >\n        <Paperclip />\n        {label(files, \"Attach a file\")}\n      </button>\n      {files.length > 0 && (\n        <button type=\"button\" aria-label=\"Remove selection\" onClick={clear} className={clearBtn}>\n          <X className=\"size-3.5\" />\n        </button>\n      )}\n    </span>\n  )\n}\n\n/* 2) MinimalFile: borderless, icon plus name only. The quietest variant. */\nexport function MinimalFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, clear } = useFiles(multiple, onFilesChange)\n  const selected = files.length > 0\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"flex min-w-0 items-center gap-1\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose a file\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"inline-flex min-w-0 select-none items-center border border-transparent bg-transparent outline-none transition-colors duration-(--motion-fast) hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n          iconCompat,\n          controlSize[size],\n          selected ? \"text-foreground\" : \"text-muted-foreground\"\n        )}\n      >\n        <Upload />\n        <span className=\"min-w-0 truncate\">{label(files, \"Upload\")}</span>\n      </button>\n      {selected && (\n        <button type=\"button\" aria-label=\"Remove selection\" onClick={clear} className={clearBtn}>\n          <X className=\"size-3.5\" />\n        </button>\n      )}\n    </div>\n  )\n}\n\n/* 3) NarrowFile: the variant that fits a form row, with the name and the clear control side by side in a narrow box. */\nexport function NarrowFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, clear } = useFiles(multiple, onFilesChange)\n  const selected = files.length > 0\n\n  return (\n    <div\n      data-slot=\"styled-file-input\"\n      className={cn(\n        \"flex w-full max-w-56 min-w-0 items-center border border-field-border bg-transparent shadow-xs transition-[color,box-shadow] duration-(--motion-fast) has-[button:focus-visible]:border-ring dark:bg-input/30 motion-reduce:transition-none\",\n        controlSize[size],\n        className\n      )}\n    >\n      <label htmlFor={id} className=\"sr-only\">\n        Choose a file\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"flex min-w-0 flex-1 select-none items-center gap-1.5 rounded-sm text-left outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          iconCompat,\n          selected ? \"text-foreground\" : \"text-muted-foreground\"\n        )}\n      >\n        <Paperclip />\n        <span className=\"min-w-0 flex-1 truncate\">{label(files, \"Choose a file\")}</span>\n      </button>\n      {selected && (\n        <button type=\"button\" aria-label=\"Clear selected file\" onClick={clear} className={clearBtn}>\n          <X className=\"size-3.5\" />\n        </button>\n      )}\n    </div>\n  )\n}\n\n/* 4) RowFile: label on the left, trigger on the right. For settings and table rows. */\nexport function RowFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div\n      data-slot=\"styled-file-input\"\n      className={cn(\n        \"flex w-full items-center justify-between gap-3 border-b border-border py-2\",\n        className\n      )}\n    >\n      <label htmlFor={id} className={cn(\"min-w-0 truncate text-muted-foreground\", textSize[size])}>\n        Attachment\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <div className=\"flex min-w-0 items-center gap-2\">\n        {files.length > 0 && (\n          <span className={cn(\"flex min-w-0 items-center gap-1.5\", textSize[size])}>\n            <span className=\"min-w-0 truncate text-foreground\">{files[0].name}</span>\n            <span className=\"shrink-0 text-xs text-muted-foreground\">{formatSize(files[0].size)}</span>\n            <button\n              type=\"button\"\n              aria-label={`Remove ${files[0].name}`}\n              onClick={() => remove(fileKey(files[0]))}\n              className={clearBtn}\n            >\n              <X className=\"size-3.5\" />\n            </button>\n          </span>\n        )}\n        <button\n          type=\"button\"\n          data-slot=\"styled-file-input-surface\"\n          onClick={() => inputRef.current?.click()}\n          className={cn(\n            \"inline-flex shrink-0 select-none items-center justify-center border border-border bg-surface-2 font-medium whitespace-nowrap text-foreground outline-none transition-colors duration-(--motion-fast) hover:bg-surface-3 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n            iconCompat,\n            controlSize[size]\n          )}\n        >\n          <Plus />\n          {files.length > 0 ? \"Replace\" : \"Add\"}\n        </button>\n      </div>\n    </div>\n  )\n}\n\n/* 5) BareFile: a square trigger carrying only an icon. The narrowest option for toolbars; when there is a selection a small counter badge appears beside it. */\nexport function BareFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, clear } = useFiles(multiple, onFilesChange)\n  const square: Record<StyledSize, string> = {\n    sm: \"size-7 rounded-md\",\n    md: \"size-8 rounded-md\",\n    lg: \"size-9 rounded-lg\",\n    xl: \"size-10 rounded-lg\",\n  }\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"inline-flex items-center gap-1.5\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Attach files\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        aria-label=\"Attach files\"\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"inline-flex shrink-0 items-center justify-center border border-transparent bg-transparent text-muted-foreground outline-none transition-colors duration-(--motion-fast) hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n          iconCompat,\n          square[size]\n        )}\n      >\n        <Paperclip />\n      </button>\n      {files.length > 0 && (\n        <>\n          <span className=\"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-primary)_14%,transparent)] px-1.5 text-xs font-medium tabular-nums text-primary\">\n            {files.length}\n          </span>\n          <button type=\"button\" aria-label=\"Remove all files\" onClick={clear} className={clearBtn}>\n            <X className=\"size-3.5\" />\n          </button>\n        </>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/file-input-compact.tsx"
    }
  ],
  "categories": [
    "styled",
    "file"
  ],
  "type": "registry:component"
}