{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload-1",
  "title": "Dropzone With Upload Progress",
  "description": "File upload card with a drag-and-drop dropzone and an attachment list showing a spinner per file while uploading and a check when finished.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "attachment",
    "button",
    "card",
    "spinner"
  ],
  "files": [
    {
      "path": "registry/blocks/file-upload/1/file-upload-block.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Attachment,\n  AttachmentAction,\n  AttachmentActions,\n  AttachmentContent,\n  AttachmentDescription,\n  AttachmentMedia,\n  AttachmentTitle,\n} from \"@/components/ui/attachment\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\ninterface UploadFile {\n  id: number\n  name: string\n  size: string\n  progress: number\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport default function FileUploadBlock() {\n  const [files, setFiles] = React.useState<UploadFile[]>([])\n  const [isDragging, setIsDragging] = React.useState(false)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const nextId = React.useRef(1)\n  const timers = React.useRef<Record<number, ReturnType<typeof setInterval>>>(\n    {}\n  )\n\n  React.useEffect(() => {\n    const running = timers.current\n    return () => {\n      Object.values(running).forEach((t) => clearInterval(t))\n    }\n  }, [])\n\n  function simulateUpload(id: number) {\n    timers.current[id] = setInterval(() => {\n      setFiles((prev) =>\n        prev.map((f) => {\n          if (f.id !== id) return f\n          const next = Math.min(100, f.progress + 8)\n          if (next >= 100) {\n            clearInterval(timers.current[id])\n            delete timers.current[id]\n          }\n          return { ...f, progress: next }\n        })\n      )\n    }, 180)\n  }\n\n  function addFiles(fileList: FileList | null) {\n    if (!fileList || fileList.length === 0) return\n    const incoming: UploadFile[] = Array.from(fileList).map((file) => ({\n      id: nextId.current++,\n      name: file.name,\n      size: formatSize(file.size),\n      progress: 0,\n    }))\n    setFiles((prev) => [...prev, ...incoming])\n    incoming.forEach((f) => simulateUpload(f.id))\n  }\n\n  function removeFile(id: number) {\n    if (timers.current[id]) {\n      clearInterval(timers.current[id])\n      delete timers.current[id]\n    }\n    setFiles((prev) => prev.filter((f) => f.id !== id))\n  }\n\n  function clearAll() {\n    Object.values(timers.current).forEach((t) => clearInterval(t))\n    timers.current = {}\n    setFiles([])\n  }\n\n  return (\n    <section className=\"flex w-full items-center justify-center bg-background px-6 py-12 text-foreground\">\n      <Card className=\"w-full max-w-md\">\n        <CardHeader>\n          <CardTitle>Upload files</CardTitle>\n          <CardDescription>\n            Attach documents, images, or other assets to this project.\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          <input\n            ref={inputRef}\n            type=\"file\"\n            multiple\n            className=\"sr-only\"\n            onChange={(e) => {\n              addFiles(e.target.files)\n              e.target.value = \"\"\n            }}\n          />\n\n          <div\n            onDragOver={(e) => {\n              e.preventDefault()\n              setIsDragging(true)\n            }}\n            onDragLeave={(e) => {\n              e.preventDefault()\n              setIsDragging(false)\n            }}\n            onDrop={(e) => {\n              e.preventDefault()\n              setIsDragging(false)\n              addFiles(e.dataTransfer.files)\n            }}\n            className={cn(\n              \"flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/40 px-6 py-10 transition-colors\",\n              isDragging && \"border-primary bg-muted\"\n            )}\n          >\n            <div className=\"flex size-10 items-center justify-center rounded-lg border border-border bg-background\">\n              <IconPlaceholder\n                lucide=\"UploadCloud\"\n                tabler=\"IconCloudUpload\"\n                hugeicons=\"CloudUploadIcon\"\n                phosphor=\"CloudArrowUp\"\n                remixicon=\"RiUploadCloud2Line\"\n                className=\"size-5 text-muted-foreground\"\n                aria-hidden=\"true\"\n              />\n            </div>\n            <div className=\"flex flex-col items-center gap-1 text-center\">\n              <p className=\"text-sm font-medium text-foreground\">\n                Drag &amp; drop files or browse\n              </p>\n              <p className=\"text-xs text-muted-foreground\">\n                Supports PDF, PNG, JPG up to 25 MB\n              </p>\n            </div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => inputRef.current?.click()}\n            >\n              <IconPlaceholder\n                lucide=\"UploadCloud\"\n                tabler=\"IconCloudUpload\"\n                hugeicons=\"CloudUploadIcon\"\n                phosphor=\"CloudArrowUp\"\n                remixicon=\"RiUploadCloud2Line\"\n                data-icon=\"inline-start\"\n                aria-hidden=\"true\"\n              />\n              Browse Files\n            </Button>\n          </div>\n\n          {files.length > 0 && (\n            <div className=\"flex flex-col gap-2\">\n              <div className=\"flex items-center justify-between\">\n                <p className=\"text-xs text-muted-foreground tabular-nums\">\n                  <span className=\"font-medium text-foreground\">\n                    {files.length}\n                  </span>{\" \"}\n                  {files.length === 1 ? \"File\" : \"Files\"}\n                </p>\n                <button\n                  type=\"button\"\n                  onClick={clearAll}\n                  className=\"text-xs font-medium text-muted-foreground transition-colors hover:text-foreground\"\n                >\n                  Clear All\n                </button>\n              </div>\n              <ul className=\"flex flex-col gap-2\">\n                {files.map((file) => {\n                  const done = file.progress >= 100\n                  return (\n                    <li key={file.id}>\n                      <Attachment\n                        state={done ? \"done\" : \"uploading\"}\n                        size=\"sm\"\n                        className=\"w-full\"\n                      >\n                        <AttachmentMedia>\n                          {done ? (\n                            <IconPlaceholder\n                              lucide=\"Check\"\n                              tabler=\"IconCheck\"\n                              hugeicons=\"Tick02Icon\"\n                              phosphor=\"Check\"\n                              remixicon=\"RiCheckLine\"\n                              className=\"text-primary\"\n                              aria-hidden=\"true\"\n                            />\n                          ) : (\n                            <Spinner />\n                          )}\n                        </AttachmentMedia>\n                        <AttachmentContent>\n                          <AttachmentTitle>{file.name}</AttachmentTitle>\n                          <AttachmentDescription className=\"tabular-nums\">\n                            {done\n                              ? `Uploaded ${file.size}`\n                              : `Uploading ${file.progress}%`}\n                          </AttachmentDescription>\n                        </AttachmentContent>\n                        <AttachmentActions>\n                          <AttachmentAction\n                            aria-label={`Remove ${file.name}`}\n                            onClick={() => removeFile(file.id)}\n                          >\n                            <IconPlaceholder\n                              lucide=\"X\"\n                              tabler=\"IconX\"\n                              hugeicons=\"Cancel01Icon\"\n                              phosphor=\"X\"\n                              remixicon=\"RiCloseLine\"\n                              aria-hidden=\"true\"\n                            />\n                          </AttachmentAction>\n                        </AttachmentActions>\n                      </Attachment>\n                    </li>\n                  )\n                })}\n              </ul>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/file-upload-1.tsx"
    }
  ],
  "meta": {
    "height": "410px",
    "tier": "free"
  },
  "categories": [
    "file-upload"
  ],
  "type": "registry:block"
}