{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload-dropzone",
  "title": "File Upload Dropzone",
  "description": "A controlled drag-and-drop file upload field with selected file rows.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://ui.corr.sh/r/animated-progress.json"
  ],
  "files": [
    {
      "path": "registry/corr/components/file-upload-dropzone.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, FileText, UploadCloud, X } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { AnimatedProgress } from \"./animated-progress\"\n\nexport type FileUploadDropzoneFile = {\n  id: string\n  file: File\n  status?: \"idle\" | \"uploading\" | \"success\" | \"error\"\n  progress?: number\n  error?: string\n}\n\nfunction getFileExtension(fileName: string) {\n  const extension = fileName.split(\".\").pop()\n  return extension && extension !== fileName\n    ? extension.slice(0, 4).toUpperCase()\n    : \"FILE\"\n}\n\nexport function FileUploadDropzone({\n  files,\n  onFilesChange,\n  accept,\n  multiple = true,\n  maxFiles = 6,\n  label = \"Drop files here\",\n  description = \"or click to browse from your device\",\n  disabled = false,\n  simulateUpload = false,\n  className,\n}: {\n  files: FileUploadDropzoneFile[]\n  onFilesChange: (files: FileUploadDropzoneFile[]) => void\n  accept?: string\n  multiple?: boolean\n  maxFiles?: number\n  label?: string\n  description?: string\n  disabled?: boolean\n  simulateUpload?: boolean\n  className?: string\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragging, setDragging] = React.useState(false)\n\n  function addFiles(nextFiles: FileList | File[]) {\n    if (disabled) return\n\n    const incoming = Array.from(nextFiles).map((file, index) => ({\n      id: `${file.name}-${file.lastModified}-${crypto.randomUUID()}`,\n      file,\n      status: simulateUpload ? (\"uploading\" as const) : (\"idle\" as const),\n      progress: simulateUpload ? 12 + index * 8 : undefined,\n    }))\n    const merged = multiple ? [...files, ...incoming] : incoming.slice(0, 1)\n    onFilesChange(merged.slice(0, maxFiles))\n  }\n\n  React.useEffect(() => {\n    if (!simulateUpload) return\n    if (!files.some((item) => item.status === \"uploading\")) return\n\n    const timer = window.setInterval(() => {\n      onFilesChange(\n        files.map((item) => {\n          if (item.status !== \"uploading\") return item\n\n          const nextProgress = Math.min(100, (item.progress ?? 0) + 18)\n          const failed = item.file.name.toLowerCase().includes(\"fail\")\n\n          if (nextProgress >= 100) {\n            return {\n              ...item,\n              progress: 100,\n              status: failed ? (\"error\" as const) : (\"success\" as const),\n              error: failed ? \"Upload failed\" : undefined,\n            }\n          }\n\n          return { ...item, progress: nextProgress }\n        })\n      )\n    }, 450)\n\n    return () => window.clearInterval(timer)\n  }, [files, onFilesChange, simulateUpload])\n\n  return (\n    <div className={cn(\"grid gap-3\", className)}>\n      <button\n        type=\"button\"\n        disabled={disabled}\n        onClick={() => inputRef.current?.click()}\n        onDragEnter={(event) => {\n          event.preventDefault()\n          if (!disabled) setDragging(true)\n        }}\n        onDragOver={(event) => event.preventDefault()}\n        onDragLeave={(event) => {\n          event.preventDefault()\n          setDragging(false)\n        }}\n        onDrop={(event) => {\n          event.preventDefault()\n          setDragging(false)\n          addFiles(event.dataTransfer.files)\n        }}\n        className={cn(\n          \"flex min-h-36 w-full flex-col items-center justify-center rounded-md border border-dashed bg-card p-6 text-center transition-colors outline-none\",\n          \"hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30\",\n          dragging && \"border-primary bg-primary/5\",\n          disabled && \"cursor-not-allowed opacity-50\"\n        )}\n      >\n        <input\n          ref={inputRef}\n          type=\"file\"\n          accept={accept}\n          multiple={multiple}\n          disabled={disabled}\n          className=\"sr-only\"\n          onChange={(event) => {\n            if (event.target.files) addFiles(event.target.files)\n            event.target.value = \"\"\n          }}\n        />\n        {files.length > 0 ? (\n          <div className=\"grid w-full gap-2 text-left\">\n            {files.map((item) => (\n              <div\n                key={item.id}\n                className=\"grid gap-2 rounded-md border bg-background/80 px-3 py-2 text-sm\"\n              >\n                <div className=\"flex min-w-0 items-center justify-between gap-3\">\n                  <div className=\"flex min-w-0 items-center gap-3\">\n                    <span className=\"relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-muted/40\">\n                      <FileText className=\"size-4 text-muted-foreground\" />\n                      <span className=\"absolute -right-1 -bottom-1 rounded-sm bg-foreground px-1 py-0.5 text-[0.5rem] leading-none font-semibold text-background\">\n                        {getFileExtension(item.file.name)}\n                      </span>\n                    </span>\n                    <span className=\"min-w-0\">\n                      <span className=\"block truncate font-medium\">\n                        {item.file.name}\n                      </span>\n                      <span className=\"block text-xs text-muted-foreground\">\n                        {(item.file.size / 1024).toFixed(1)} KB\n                      </span>\n                    </span>\n                  </div>\n                  <div className=\"flex shrink-0 items-center gap-2\">\n                    {item.status === \"success\" ? (\n                      <span className=\"flex size-7 items-center justify-center rounded-md bg-emerald-500/10 text-emerald-600\">\n                        <Check className=\"size-3.5\" />\n                      </span>\n                    ) : item.status === \"error\" ? (\n                      <span className=\"flex size-7 items-center justify-center rounded-md bg-destructive/10 text-destructive\">\n                        <X className=\"size-3.5\" />\n                      </span>\n                    ) : null}\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"icon-sm\"\n                      onClick={(event) => {\n                        event.stopPropagation()\n                        onFilesChange(\n                          files.filter((file) => file.id !== item.id)\n                        )\n                      }}\n                    >\n                      <X />\n                      <span className=\"sr-only\">Remove {item.file.name}</span>\n                    </Button>\n                  </div>\n                </div>\n                {item.status === \"uploading\" ? (\n                  <AnimatedProgress value={item.progress ?? 0} showValue />\n                ) : item.status === \"error\" && item.error ? (\n                  <div className=\"text-xs text-destructive\">{item.error}</div>\n                ) : null}\n              </div>\n            ))}\n          </div>\n        ) : (\n          <>\n            <span className=\"flex size-10 items-center justify-center rounded-md border bg-background\">\n              <UploadCloud className=\"size-4 text-muted-foreground\" />\n            </span>\n            <span className=\"mt-3 text-sm font-medium\">{label}</span>\n            <span className=\"mt-1 text-xs text-muted-foreground\">\n              {description}\n            </span>\n          </>\n        )}\n      </button>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}