{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input-tone",
  "title": "Input-tone file",
  "description": "Styled file: the Input-tone set. 5 decorative file variations (InfoFile, SuccessFile, WarningFile, DangerFile, MutedFile) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/file-input-tone.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport {\n  AlertTriangle,\n  CheckCircle2,\n  File as FileIcon,\n  Info,\n  ShieldAlert,\n  Upload,\n  X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Tone file-input family: decorative pickers carrying the same dropzone anatomy in\n   5 semantic tones. The tone changes only the color/icon/hint text; the anatomy and\n   the behavior are identical. Each export wraps a real <input type=\"file\"> and\n   supports real drag and drop (preventDefault). Color comes ONLY from semantic\n   tokens, with transparency via color-mix. The invalid appearance uses the ai2\n   danger tokens (not destructive). The animations are disabled through\n   useReducedMotion. Deterministic. */\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  return { files, add, remove }\n}\n\nconst dropSurface: Record<StyledSize, string> = {\n  sm: \"gap-2 rounded-lg p-4 text-sm\",\n  md: \"gap-3 rounded-xl p-6 text-sm\",\n  lg: \"gap-3 rounded-2xl p-8 text-base\",\n  xl: \"gap-4 rounded-2xl p-10 text-base\",\n}\n\nconst iconSize: Record<StyledSize, string> = {\n  sm: \"size-5\",\n  md: \"size-6\",\n  lg: \"size-8\",\n  xl: \"size-9\",\n}\n\nconst clearBtn =\n  \"relative shrink-0 rounded-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:-inset-1.5 motion-reduce:transition-none\"\n\ntype Tone = \"info\" | \"success\" | \"warning\" | \"danger\" | \"muted\"\n\n/* Surface, icon and list class per tone. Transparency only through color-mix. */\nconst toneSurface: Record<Tone, string> = {\n  info: \"border-info bg-[color-mix(in_oklab,var(--color-info)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--color-info)_10%,transparent)] data-[drag-active]:bg-[color-mix(in_oklab,var(--color-info)_16%,transparent)]\",\n  success:\n    \"border-success bg-[color-mix(in_oklab,var(--color-success)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--color-success)_10%,transparent)] data-[drag-active]:bg-[color-mix(in_oklab,var(--color-success)_16%,transparent)]\",\n  warning:\n    \"border-warning bg-[color-mix(in_oklab,var(--color-warning)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--color-warning)_10%,transparent)] data-[drag-active]:bg-[color-mix(in_oklab,var(--color-warning)_16%,transparent)]\",\n  danger:\n    \"border-danger bg-[color-mix(in_oklab,var(--color-danger)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--color-danger)_10%,transparent)] data-[drag-active]:bg-[color-mix(in_oklab,var(--color-danger)_16%,transparent)]\",\n  muted:\n    \"border-border bg-[color-mix(in_oklab,var(--color-foreground)_4%,transparent)] hover:bg-[color-mix(in_oklab,var(--color-foreground)_7%,transparent)] data-[drag-active]:bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]\",\n}\n\nconst toneIcon: Record<Tone, string> = {\n  info: \"text-info\",\n  success: \"text-success\",\n  warning: \"text-warning\",\n  danger: \"text-danger\",\n  muted: \"text-muted-foreground\",\n}\n\nconst toneChip: Record<Tone, string> = {\n  info: \"bg-[color-mix(in_oklab,var(--color-info)_14%,transparent)] text-info\",\n  success: \"bg-[color-mix(in_oklab,var(--color-success)_14%,transparent)] text-success\",\n  warning: \"bg-[color-mix(in_oklab,var(--color-warning)_14%,transparent)] text-warning\",\n  danger: \"bg-[color-mix(in_oklab,var(--color-danger)_14%,transparent)] text-danger\",\n  muted: \"bg-surface-3 text-muted-foreground\",\n}\n\n/* Shared shell: a hidden real input plus a focusable dropzone button plus the file list. */\nfunction ToneShell({\n  tone,\n  icon,\n  headline,\n  hint,\n  className,\n  size = \"md\",\n  multiple = true,\n  accept,\n  onFilesChange,\n}: FileInputProps & {\n  tone: Tone\n  icon: React.ReactNode\n  headline: string\n  hint: string\n}) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" data-tone={tone} className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        {headline}\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        data-tone={tone}\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        className={cn(\n          \"flex w-full flex-col items-center justify-center border border-dashed text-center text-muted-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[drag-active]:text-foreground motion-reduce:transition-none [&>svg]:shrink-0 [&>i]:leading-none\",\n          toneSurface[tone],\n          dropSurface[size]\n        )}\n      >\n        <span className={cn(\"flex items-center justify-center\", iconSize[size], toneIcon[tone])}>\n          {icon}\n        </span>\n        <span className=\"font-medium text-foreground\">{headline}</span>\n        <span className=\"text-xs text-muted-foreground\">{hint}</span>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {files.length > 0 && (\n          <motion.ul\n            data-slot=\"styled-file-input-list\"\n            className=\"mt-3 flex flex-col gap-2\"\n            initial={reduce ? undefined : { opacity: 0, height: 0 }}\n            animate={reduce ? undefined : { opacity: 1, height: \"auto\" }}\n            exit={reduce ? undefined : { opacity: 0, height: 0 }}\n          >\n            {files.map((file) => (\n              <li\n                key={fileKey(file)}\n                className=\"flex items-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm\"\n              >\n                <span\n                  className={cn(\n                    \"flex size-7 shrink-0 items-center justify-center rounded-md\",\n                    toneChip[tone]\n                  )}\n                >\n                  <FileIcon className=\"size-4\" />\n                </span>\n                <span className=\"min-w-0 flex-1 truncate text-foreground\">{file.name}</span>\n                <span className=\"shrink-0 text-xs text-muted-foreground\">{formatSize(file.size)}</span>\n                <button\n                  type=\"button\"\n                  aria-label={`Remove ${file.name}`}\n                  onClick={() => remove(fileKey(file))}\n                  className={clearBtn}\n                >\n                  <X className=\"size-4\" />\n                </button>\n              </li>\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* 1) InfoFile: notr bilgilendirici ton, info token'lari. */\nexport function InfoFile(props: FileInputProps) {\n  return (\n    <ToneShell\n      {...props}\n      tone=\"info\"\n      icon={<Info className=\"size-full\" />}\n      headline=\"Drop files to attach them\"\n      hint=\"Anything you attach stays private to this record\"\n    />\n  )\n}\n\n/* 2) SuccessFile: onaylanmis/kabul edilmis ton, success token'lari. */\nexport function SuccessFile(props: FileInputProps) {\n  return (\n    <ToneShell\n      {...props}\n      tone=\"success\"\n      icon={<CheckCircle2 className=\"size-full\" />}\n      headline=\"Drop verified documents here\"\n      hint=\"These file types are accepted\"\n    />\n  )\n}\n\n/* 3) WarningFile: dikkat isteyen ton, warning token'lari. */\nexport function WarningFile(props: FileInputProps) {\n  return (\n    <ToneShell\n      {...props}\n      tone=\"warning\"\n      icon={<AlertTriangle className=\"size-full\" />}\n      headline=\"Drop files with care\"\n      hint=\"Large files may take a while to process\"\n    />\n  )\n}\n\n/* 4) DangerFile: the risky or rejected tone, ai2 danger tokens (not destructive). */\nexport function DangerFile(props: FileInputProps) {\n  return (\n    <ToneShell\n      {...props}\n      tone=\"danger\"\n      icon={<ShieldAlert className=\"size-full\" />}\n      headline=\"Restricted upload area\"\n      hint=\"Unsupported files will be rejected\"\n    />\n  )\n}\n\n/* 5) MutedFile: sessiz/ikincil ton, notr foreground karisimlari. */\nexport function MutedFile(props: FileInputProps) {\n  return (\n    <ToneShell\n      {...props}\n      tone=\"muted\"\n      icon={<Upload className=\"size-full\" />}\n      headline=\"Optional attachments\"\n      hint=\"You can skip this step\"\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/file-input-tone.tsx"
    }
  ],
  "categories": [
    "styled",
    "file"
  ],
  "type": "registry:component"
}