{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-dropzone",
  "title": "File Dropzone",
  "description": "Drag-and-drop file upload with size checks and a file list.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/file-dropzone.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { FileIcon, UploadCloudIcon, XIcon } from \"lucide-react\";\n\nimport { enterChildren, gsap, marksIn, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype FileDropzoneProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** Called with the accepted files (after accept/size checks). */\n  onFilesChange?: (files: File[]) => void;\n  accept?: string;\n  multiple?: boolean;\n  /** Maximum size per file in bytes. */\n  maxSize?: number;\n  hint?: React.ReactNode;\n};\n\nconst formatBytes = (n: number) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1048576).toFixed(1)} MB`);\n\n/**\n * Drag-and-drop upload area. While a file is dragged over, the crop marks lock onto the zone and the icon lifts;\n * dropped files list in with a stagger and can be removed. Click or press Enter to browse instead.\n */\nfunction FileDropzone({ onFilesChange, accept, multiple = true, maxSize, hint, className, ...props }: FileDropzoneProps) {\n  const [files, setFiles] = React.useState<File[]>([]);\n  const [over, setOver] = React.useState(false);\n  const [error, setError] = React.useState<string | null>(null);\n  const zone = React.useRef<HTMLDivElement>(null);\n  const input = React.useRef<HTMLInputElement>(null);\n  const list = React.useRef<HTMLUListElement>(null);\n  const before = React.useRef(0);\n\n  const take = (incoming: FileList | null) => {\n    if (!incoming) return;\n    const ok: File[] = [];\n    const rejected: string[] = [];\n    for (const f of Array.from(incoming)) {\n      if (maxSize && f.size > maxSize) rejected.push(`${f.name} is over ${formatBytes(maxSize)}`);\n      else ok.push(f);\n    }\n    setError(rejected[0] ?? null);\n    const next = multiple ? [...files, ...ok] : ok.slice(0, 1);\n    before.current = multiple ? files.length : 0;\n    setFiles(next);\n    onFilesChange?.(next);\n  };\n\n  React.useLayoutEffect(() => {\n    const items = list.current ? Array.from(list.current.children).slice(before.current) : [];\n    if (items.length && list.current && motionScale(list.current)) enterChildren(items, { opacity: 0, y: 8 }, { stagger: 0.05 });\n    before.current = files.length;\n  }, [files.length]);\n\n  React.useEffect(() => {\n    const el = zone.current;\n    if (!el || !motionScale(el)) return;\n    if (over) marksIn(el);\n    gsap.to(el.querySelector('[data-slot=\"dropzone-icon\"]'), { y: over ? -6 : 0, scale: over ? 1.08 : 1, duration: 0.25, ease: \"cue\" });\n  }, [over]);\n\n  return (\n    <div data-slot=\"file-dropzone\" className={cn(\"grid gap-3\", className)} {...props}>\n      <div\n        ref={zone}\n        role=\"button\"\n        tabIndex={0}\n        aria-label=\"Upload files: drop them here or press Enter to browse\"\n        data-over={over || undefined}\n        onClick={() => input.current?.click()}\n        onKeyDown={(e) => (e.key === \"Enter\" || e.key === \" \") && (e.preventDefault(), input.current?.click())}\n        onDragEnter={(e) => (e.preventDefault(), setOver(true))}\n        onDragOver={(e) => e.preventDefault()}\n        onDragLeave={(e) => !e.currentTarget.contains(e.relatedTarget as Node) && setOver(false)}\n        onDrop={(e) => {\n          e.preventDefault();\n          setOver(false);\n          take(e.dataTransfer.files);\n        }}\n        className=\"relative grid cursor-pointer justify-items-center gap-2 rounded-2xl border border-dashed border-input bg-muted/30 px-6 py-10 text-center outline-none transition-colors duration-200 hover:bg-muted/60 focus-visible:ring-3 focus-visible:ring-ring/50 data-[over]:cue-marks data-[over]:border-ring data-[over]:bg-signal/10\"\n      >\n        <span data-slot=\"dropzone-icon\" className=\"grid size-11 place-items-center rounded-xl border border-border bg-card shadow-raised\">\n          <UploadCloudIcon className=\"size-5\" />\n        </span>\n        <p className=\"text-sm font-medium\">{over ? \"Drop to upload\" : \"Drop files here or click to browse\"}</p>\n        <p className=\"text-xs text-muted-foreground\">{hint ?? (maxSize ? `Up to ${formatBytes(maxSize)} each` : \"Any file type\")}</p>\n        <input ref={input} type=\"file\" accept={accept} multiple={multiple} className=\"sr-only\" tabIndex={-1} onChange={(e) => take(e.target.files)} />\n      </div>\n      {error && (\n        <p role=\"alert\" className=\"text-xs text-destructive\">\n          {error}\n        </p>\n      )}\n      {files.length > 0 && (\n        <ul ref={list} className=\"grid gap-2\">\n          {files.map((f, i) => (\n            <li key={`${f.name}-${i}`} className=\"flex items-center gap-3 rounded-lg border border-border bg-card p-2.5 text-sm shadow-control\">\n              <span className=\"grid size-8 place-items-center rounded-md bg-muted\">\n                <FileIcon className=\"size-4 text-muted-foreground\" />\n              </span>\n              <span className=\"grid min-w-0 flex-1\">\n                <span className=\"truncate font-medium\">{f.name}</span>\n                <span className=\"font-mono text-[0.6875rem] text-muted-foreground\">{formatBytes(f.size)}</span>\n              </span>\n              <button\n                type=\"button\"\n                aria-label={`Remove ${f.name}`}\n                onClick={() => {\n                  const next = files.filter((_, j) => j !== i);\n                  setFiles(next);\n                  onFilesChange?.(next);\n                }}\n                className=\"grid size-7 place-items-center rounded-md text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50\"\n              >\n                <XIcon className=\"size-4\" />\n              </button>\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n}\n\nexport { FileDropzone };\nexport type { FileDropzoneProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/file-dropzone",
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}