{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input-styled",
  "title": "Input-styled file",
  "description": "Styled file: the Input-styled set. 5 decorative file variations (DropzoneFile, ButtonFile, AvatarFile, ListFile, GlassFile) 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-styled.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { File, Image, Upload, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Styled file-input family: 5 decorative/opt-in file pickers. Each wraps a hidden\n   real <input type=\"file\"> and triggers it from a styled surface; the selected\n   files are held in state (from the input's onChange files) and each one gets a\n   remove (X) control. The dropzone-based ones open a token \"drag active\" state\n   through onDragOver/onDrop (preventDefault). Color comes ONLY from semantic tokens\n   (transparency via color-mix), size = the surface padding/scale. The animations\n   use framer-motion and are disabled under reduced-motion. Deterministic (no\n   Date.now/Math.random). */\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\n/* Ortak dosya-state mantigi: secim ekle/degistir/kaldir + disari haber ver. */\nfunction useFiles(multiple: boolean | undefined, onFilesChange?: (files: File[]) => void) {\n  const [files, setFiles] = React.useState<File[]>([])\n\n  const commit = React.useCallback(\n    (next: File[]) => {\n      setFiles(next)\n      onFilesChange?.(next)\n    },\n    [onFilesChange]\n  )\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      if (!multiple) {\n        commit([list[0]])\n        return\n      }\n      setFiles((prev) => {\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, commit, 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(() => commit([]), [commit])\n\n  return { files, add, remove, clear }\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\n/* 1) DropzoneFile: a large dashed token dropzone with an Upload icon plus a hint, supporting drag and drop. While drag is active it takes border-primary plus a primary tint. */\nexport function DropzoneFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\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\" className={cn(\"w-full\", className)}>\n      <input\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-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 border-border bg-transparent text-center text-muted-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) hover:border-primary/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[drag-active]:border-primary data-[drag-active]:bg-[color-mix(in_oklab,var(--color-primary)_10%,transparent)] data-[drag-active]:text-foreground motion-reduce:transition-none\",\n          dropSurface[size]\n        )}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop files here or click to browse</span>\n        <span className=\"text-xs text-muted-foreground\">{accept ? accept : \"Any file type\"}</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                <File className=\"size-4 shrink-0 text-muted-foreground\" />\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=\"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                >\n                  <X className=\"size-4\" />\n                </button>\n              </li>\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nconst buttonSize: Record<StyledSize, string> = {\n  sm: \"h-8 gap-1.5 rounded-md px-3 text-sm\",\n  md: \"h-9 gap-2 rounded-lg px-4 text-sm\",\n  lg: \"h-10 gap-2 rounded-lg px-5 text-sm\",\n  xl: \"h-12 gap-2.5 rounded-xl px-6 text-base\",\n}\n\n/* 2) ButtonFile: kompakt \"Choose file\" butonu + yanindaki secili dosya adi. */\nexport function ButtonFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"flex items-center gap-3\", className)}>\n      <input\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 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-base) ease-(--motion-ease) hover:bg-surface-3 hover:border-primary/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\",\n          buttonSize[size]\n        )}\n      >\n        <Upload />\n        Choose file{multiple ? \"s\" : \"\"}\n      </button>\n\n      {files.length > 0 ? (\n        <div className=\"flex min-w-0 items-center gap-2 text-sm\">\n          <span className=\"min-w-0 truncate text-foreground\">\n            {files.length === 1 ? files[0].name : `${files.length} files selected`}\n          </span>\n          <button\n            type=\"button\"\n            aria-label=\"Remove selection\"\n            onClick={() => (files.length === 1 ? remove(fileKey(files[0])) : files.forEach((f) => remove(fileKey(f))))}\n            className=\"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          >\n            <X className=\"size-4\" />\n          </button>\n        </div>\n      ) : (\n        <span className=\"truncate text-sm text-muted-foreground\">No file chosen</span>\n      )}\n    </div>\n  )\n}\n\nconst avatarSize: Record<StyledSize, string> = {\n  sm: \"size-16\",\n  md: \"size-20\",\n  lg: \"size-24\",\n  xl: \"size-28\",\n}\n\n/* 3) AvatarFile: a circular image-upload target showing a token placeholder, for a single image. It previews the selected image with a remove (X) on top. */\nexport function AvatarFile({ className, size = \"md\", accept = \"image/*\", onFilesChange }: FileInputProps) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [preview, setPreview] = React.useState<string | null>(null)\n  const { files, add, clear } = useFiles(false, onFilesChange)\n\n  React.useEffect(() => {\n    const current = files[0]\n    if (!current) {\n      setPreview(null)\n      return\n    }\n    const url = URL.createObjectURL(current)\n    setPreview(url)\n    return () => URL.revokeObjectURL(url)\n  }, [files])\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"relative inline-block\", className)}>\n      <input\n        ref={inputRef}\n        type=\"file\"\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=\"Upload image\"\n        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"group flex items-center justify-center overflow-hidden rounded-full border border-dashed border-border bg-surface-2 text-muted-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) hover:border-primary hover:text-primary focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n          avatarSize[size]\n        )}\n      >\n        {preview ? (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img src={preview} alt={files[0]?.name ?? \"Selected image\"} className=\"size-full object-cover\" />\n        ) : (\n          <Image className={iconSize[size]} />\n        )}\n      </button>\n      {files.length > 0 && (\n        <button\n          type=\"button\"\n          aria-label=\"Remove image\"\n          onClick={clear}\n          className=\"relative absolute -right-1 -top-1 flex size-6 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-sm 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        >\n          <X className=\"size-3.5\" />\n        </button>\n      )}\n    </div>\n  )\n}\n\n/* 4) ListFile: a (multiple) dropzone that lists the selected files as token rows,\n   each row with a remove (X) control. Drag and drop is supported. */\nexport function ListFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\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\" className={cn(\"w-full\", className)}>\n      <input\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-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 items-center justify-center border border-dashed border-border bg-transparent text-center text-muted-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) hover:border-primary/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[drag-active]:border-primary data-[drag-active]:bg-[color-mix(in_oklab,var(--color-primary)_10%,transparent)] data-[drag-active]:text-foreground motion-reduce:transition-none\",\n          dropSurface[size]\n        )}\n      >\n        <Upload className={cn(\"shrink-0\", iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Add files</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 }}\n            animate={reduce ? undefined : { opacity: 1 }}\n            exit={reduce ? undefined : { opacity: 0 }}\n          >\n            <AnimatePresence initial={false}>\n              {files.map((file) => (\n                <motion.li\n                  key={fileKey(file)}\n                  layout={!reduce}\n                  initial={reduce ? undefined : { opacity: 0, x: -8 }}\n                  animate={reduce ? undefined : { opacity: 1, x: 0 }}\n                  exit={reduce ? undefined : { opacity: 0, x: 8 }}\n                  className=\"flex items-center gap-3 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm\"\n                >\n                  <span className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-primary\">\n                    <File className=\"size-4\" />\n                  </span>\n                  <span className=\"flex min-w-0 flex-1 flex-col\">\n                    <span className=\"truncate text-foreground\">{file.name}</span>\n                    <span className=\"text-xs text-muted-foreground\">{formatSize(file.size)}</span>\n                  </span>\n                  <button\n                    type=\"button\"\n                    aria-label={`Remove ${file.name}`}\n                    onClick={() => remove(fileKey(file))}\n                    className=\"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                  >\n                    <X className=\"size-4\" />\n                  </button>\n                </motion.li>\n              ))}\n            </AnimatePresence>\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* 5) GlassFile: buzlu cam (frosted) dropzone kart. backdrop-filter desteklenmezse\n   opak surface'a duser. Surukle birak destekli. */\nexport function GlassFile({ className, size = \"md\", multiple, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\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\" className={cn(\"w-full\", className)}>\n      <input\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-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          \"relative flex w-full flex-col items-center justify-center overflow-hidden border border-border bg-surface-2/70 text-center text-muted-foreground shadow-[inset_0_1px_0_color-mix(in_oklab,var(--color-foreground)_12%,transparent)] outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-1/2 before:rounded-[inherit] before:bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--color-foreground)_8%,transparent),transparent)] hover:border-primary/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 supports-[backdrop-filter]:bg-surface-2/40 supports-[backdrop-filter]:backdrop-blur-xl data-[drag-active]:border-primary data-[drag-active]:bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] data-[drag-active]:text-foreground motion-reduce:transition-none\",\n          dropSurface[size]\n        )}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop files onto the glass</span>\n        <span className=\"text-xs text-muted-foreground\">{accept ? accept : \"Any file type\"}</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/70 px-3 py-2 text-sm supports-[backdrop-filter]:bg-surface-2/40 supports-[backdrop-filter]:backdrop-blur-md\"\n              >\n                <File className=\"size-4 shrink-0 text-muted-foreground\" />\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=\"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                >\n                  <X className=\"size-4\" />\n                </button>\n              </li>\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/file-input-styled.tsx"
    }
  ],
  "categories": [
    "styled",
    "file"
  ],
  "type": "registry:component"
}