{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input-preview",
  "title": "Input-preview file",
  "description": "Styled file: the Input-preview set. 5 decorative file variations (ImagePreviewFile, GridFile, ThumbFile, MediaFile, CardFile) 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-preview.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { File as FileIcon, Film, Image as ImageIcon, Music, Upload, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Preview file-input family: 5 decorative pickers that show a preview of the\n   selected file. For real selected images URL.createObjectURL is used, and the URL\n   is ALWAYS released with URL.revokeObjectURL in the effect cleanup (otherwise\n   memory leaks). In the default state with no props it draws token-based\n   placeholder surfaces rather than a remote URL. Color comes ONLY from semantic\n   tokens (transparency via color-mix). The animations are disabled through\n   useReducedMotion. Deterministic (no Date.now or 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\nfunction isImage(file: File): boolean {\n  return file.type.startsWith(\"image/\")\n}\n\n/* Ortak dosya-state mantigi. */\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\n/* Object URL map for the selected images. Every URL is revoked on cleanup so no memory leaks. */\nfunction useObjectUrls(files: File[]): Record<string, string> {\n  const [urls, setUrls] = React.useState<Record<string, string>>({})\n  const signature = files.map(fileKey).join(\"|\")\n\n  React.useEffect(() => {\n    const created: string[] = []\n    const next: Record<string, string> = {}\n    for (const file of files) {\n      if (!isImage(file)) continue\n      const url = URL.createObjectURL(file)\n      created.push(url)\n      next[fileKey(file)] = url\n    }\n    setUrls(next)\n    return () => {\n      for (const url of created) URL.revokeObjectURL(url)\n      setUrls({})\n    }\n    // signature dosya listesini stabil sekilde temsil eder.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [signature])\n\n  return urls\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 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 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\nconst surfaceBase =\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 [&>svg]:shrink-0 [&>i]:leading-none\"\n\n/* A token-based placeholder surface: no remote image, only a token texture. */\nfunction PlaceholderSurface({ className }: { className?: string }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"flex items-center justify-center bg-[linear-gradient(135deg,color-mix(in_oklab,var(--color-foreground)_8%,transparent),color-mix(in_oklab,var(--color-primary)_10%,transparent))] text-muted-foreground\",\n        className\n      )}\n    >\n      <ImageIcon className=\"size-5\" />\n    </span>\n  )\n}\n\n/* 1) ImagePreviewFile: tek gorsel secimi, buyuk onizleme paneli. */\nexport function ImagePreviewFile({ className, size = \"md\", accept = \"image/*\", onFilesChange }: FileInputProps) {\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(false, onFilesChange)\n  const urls = useObjectUrls(files)\n  const current = files[0]\n  const url = current ? urls[fileKey(current)] : undefined\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose an image\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      {current ? (\n        <div className=\"relative overflow-hidden rounded-xl border border-border bg-surface-2\">\n          {url ? (\n            // eslint-disable-next-line @next/next/no-img-element\n            <img src={url} alt={current.name} className=\"h-48 w-full object-cover\" />\n          ) : (\n            <PlaceholderSurface className=\"h-48 w-full\" />\n          )}\n          <div className=\"flex items-center gap-2 border-t border-border px-3 py-2 text-sm\">\n            <FileIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            <span className=\"min-w-0 flex-1 truncate text-foreground\">{current.name}</span>\n            <span className=\"shrink-0 text-xs text-muted-foreground\">{formatSize(current.size)}</span>\n            <button\n              type=\"button\"\n              aria-label={`Remove ${current.name}`}\n              onClick={() => remove(fileKey(current))}\n              className={clearBtn}\n            >\n              <X className=\"size-4\" />\n            </button>\n          </div>\n        </div>\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(surfaceBase, dropSurface[size])}\n        >\n          <ImageIcon className={cn(iconSize[size], \"text-primary\")} />\n          <span className=\"font-medium text-foreground\">Drop an image to preview it</span>\n          <span className=\"text-xs text-muted-foreground\">PNG, JPG or GIF</span>\n        </button>\n      )}\n    </div>\n  )\n}\n\n/* 2) GridFile: cok dosyali secim, kare onizleme izgarasi. Gorsel olmayanlar\n   token placeholder ile temsil edilir. */\nexport function GridFile({ className, size = \"md\", multiple = true, accept = \"image/*\", onFilesChange }: FileInputProps) {\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  const urls = useObjectUrls(files)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose images\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-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(surfaceBase, dropSurface[size])}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop images to build a grid</span>\n      </button>\n\n      {files.length > 0 && (\n        <ul data-slot=\"styled-file-input-list\" className=\"mt-3 grid grid-cols-3 gap-2 sm:grid-cols-4\">\n          <AnimatePresence initial={false}>\n            {files.map((file) => {\n              const url = urls[fileKey(file)]\n              return (\n                <motion.li\n                  key={fileKey(file)}\n                  layout={!reduce}\n                  initial={reduce ? undefined : { opacity: 0, scale: 0.94 }}\n                  animate={reduce ? undefined : { opacity: 1, scale: 1 }}\n                  exit={reduce ? undefined : { opacity: 0, scale: 0.94 }}\n                  className=\"relative aspect-square overflow-hidden rounded-lg border border-border bg-surface-2\"\n                >\n                  {url ? (\n                    // eslint-disable-next-line @next/next/no-img-element\n                    <img src={url} alt={file.name} className=\"size-full object-cover\" />\n                  ) : (\n                    <PlaceholderSurface className=\"size-full\" />\n                  )}\n                  <button\n                    type=\"button\"\n                    aria-label={`Remove ${file.name}`}\n                    onClick={() => remove(fileKey(file))}\n                    className=\"relative absolute right-1 top-1 flex size-5 items-center justify-center rounded-full border border-border bg-background 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-3\" />\n                  </button>\n                </motion.li>\n              )\n            })}\n          </AnimatePresence>\n        </ul>\n      )}\n    </div>\n  )\n}\n\nconst thumbSize: Record<StyledSize, string> = {\n  sm: \"size-8\",\n  md: \"size-10\",\n  lg: \"size-12\",\n  xl: \"size-14\",\n}\n\nconst rowSize: Record<StyledSize, string> = {\n  sm: \"gap-2 rounded-md p-2 text-sm\",\n  md: \"gap-3 rounded-lg p-3 text-sm\",\n  lg: \"gap-3 rounded-lg p-4 text-base\",\n  xl: \"gap-4 rounded-xl p-5 text-base\",\n}\n\n/* 3) ThumbFile: her satirda kucuk kare kucuk resim + ad + boyut. */\nexport function ThumbFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n  const urls = useObjectUrls(files)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose 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        onClick={() => inputRef.current?.click()}\n        className={cn(\n          \"flex w-full items-center justify-center gap-2 border border-border bg-surface-2 font-medium text-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) 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          rowSize[size]\n        )}\n      >\n        <Upload />\n        Choose files\n      </button>\n\n      {files.length > 0 && (\n        <ul data-slot=\"styled-file-input-list\" className=\"mt-3 flex flex-col gap-2\">\n          <AnimatePresence initial={false}>\n            {files.map((file) => {\n              const url = urls[fileKey(file)]\n              return (\n                <motion.li\n                  key={fileKey(file)}\n                  layout={!reduce}\n                  initial={reduce ? undefined : { opacity: 0, y: -4 }}\n                  animate={reduce ? undefined : { opacity: 1, y: 0 }}\n                  exit={reduce ? undefined : { opacity: 0, y: -4 }}\n                  className=\"flex items-center gap-3 rounded-lg border border-border bg-surface-2 p-2 text-sm\"\n                >\n                  {url ? (\n                    // eslint-disable-next-line @next/next/no-img-element\n                    <img\n                      src={url}\n                      alt={file.name}\n                      className={cn(\"shrink-0 rounded-md object-cover\", thumbSize[size])}\n                    />\n                  ) : (\n                    <PlaceholderSurface className={cn(\"shrink-0 rounded-md\", thumbSize[size])} />\n                  )}\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={clearBtn}\n                  >\n                    <X className=\"size-4\" />\n                  </button>\n                </motion.li>\n              )\n            })}\n          </AnimatePresence>\n        </ul>\n      )}\n    </div>\n  )\n}\n\n/* Dosya turune gore ikon: gorsel / video / ses / diger. */\nfunction KindIcon({ file, className }: { file: File; className?: string }) {\n  if (file.type.startsWith(\"video/\")) return <Film className={className} />\n  if (file.type.startsWith(\"audio/\")) return <Music className={className} />\n  if (file.type.startsWith(\"image/\")) return <ImageIcon className={className} />\n  return <FileIcon className={className} />\n}\n\n/* 4) MediaFile: medya turunu (gorsel/video/ses) ayirt eden onizleme kartlari. */\nexport function MediaFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\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  const urls = useObjectUrls(files)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose media 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        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(surfaceBase, dropSurface[size])}\n      >\n        <Film className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop media files</span>\n        <span className=\"text-xs text-muted-foreground\">Images, video or audio</span>\n      </button>\n\n      {files.length > 0 && (\n        <ul data-slot=\"styled-file-input-list\" className=\"mt-3 flex flex-col gap-2\">\n          {files.map((file) => {\n            const url = urls[fileKey(file)]\n            return (\n              <li\n                key={fileKey(file)}\n                className=\"flex items-center gap-3 overflow-hidden rounded-lg border border-border bg-surface-2 p-2 text-sm\"\n              >\n                {url ? (\n                  // eslint-disable-next-line @next/next/no-img-element\n                  <img src={url} alt={file.name} className=\"size-12 shrink-0 rounded-md object-cover\" />\n                ) : (\n                  <span className=\"flex size-12 shrink-0 items-center justify-center rounded-md bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-primary\">\n                    <KindIcon file={file} className=\"size-5\" />\n                  </span>\n                )}\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\">\n                    {file.type || \"Unknown type\"} - {formatSize(file.size)}\n                  </span>\n                </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          })}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nconst cardSize: Record<StyledSize, string> = {\n  sm: \"h-28\",\n  md: \"h-36\",\n  lg: \"h-44\",\n  xl: \"h-52\",\n}\n\n/* 5) CardFile: tek dosyalik, kapak gorseli ustte olan onizleme karti. */\nexport function CardFile({ className, size = \"md\", accept, onFilesChange }: FileInputProps) {\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(false, onFilesChange)\n  const urls = useObjectUrls(files)\n  const current = files[0]\n  const url = current ? urls[fileKey(current)] : undefined\n\n  return (\n    <div\n      data-slot=\"styled-file-input\"\n      data-drag-active={dragActive || undefined}\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        \"w-full overflow-hidden rounded-xl border border-border bg-surface-2 transition-colors duration-(--motion-base) ease-(--motion-ease) data-[drag-active]:border-primary motion-reduce:transition-none\",\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        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <motion.div\n        initial={false}\n        animate={reduce ? undefined : { opacity: 1 }}\n        className={cn(\"w-full overflow-hidden\", cardSize[size])}\n      >\n        {url ? (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img src={url} alt={current?.name ?? \"Selected file\"} className=\"size-full object-cover\" />\n        ) : (\n          <PlaceholderSurface className=\"size-full\" />\n        )}\n      </motion.div>\n      <div className=\"flex items-center gap-3 border-t border-border px-3 py-3\">\n        <span className=\"flex min-w-0 flex-1 flex-col text-sm\">\n          <span className=\"truncate font-medium text-foreground\">\n            {current ? current.name : \"No file chosen\"}\n          </span>\n          <span className=\"text-xs text-muted-foreground\">\n            {current ? formatSize(current.size) : \"Drop a file or pick one\"}\n          </span>\n        </span>\n        {current ? (\n          <button\n            type=\"button\"\n            aria-label={`Remove ${current.name}`}\n            onClick={() => remove(fileKey(current))}\n            className={clearBtn}\n          >\n            <X className=\"size-4\" />\n          </button>\n        ) : null}\n        <button\n          type=\"button\"\n          data-slot=\"styled-file-input-surface\"\n          onClick={() => inputRef.current?.click()}\n          className=\"inline-flex h-8 shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-border bg-background px-3 text-sm font-medium text-foreground outline-none transition-colors 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        >\n          <Upload />\n          Browse\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/file-input-preview.tsx"
    }
  ],
  "categories": [
    "styled",
    "file"
  ],
  "type": "registry:component"
}