{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload-3",
  "title": "Uploader With Validation Toasts",
  "description": "Drag-and-drop file uploader enforcing size and type validation, with uploading, done, and error attachment states, toasts, and per-file remove.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "attachment",
    "button",
    "card",
    "sonner",
    "spinner"
  ],
  "files": [
    {
      "path": "registry/blocks/file-upload/3/file-upload-block.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { toast } from \"sonner\"\n\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 { Toaster } from \"@/components/ui/sonner\"\nimport { cn } from \"@/lib/utils\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\nconst MAX_SIZE = 25 * 1024 * 1024\nconst ACCEPTED = [\"image/png\", \"image/jpeg\", \"application/pdf\"]\n\ntype UploadStatus = \"uploading\" | \"done\" | \"error\"\n\ntype UploadItem = {\n  id: string\n  name: string\n  size: number\n  progress: number\n  status: UploadStatus\n  error?: string\n}\n\nfunction formatSize(bytes: number) {\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\nfunction validate(file: File) {\n  if (!ACCEPTED.includes(file.type)) return \"Unsupported file type\"\n  if (file.size > MAX_SIZE) return \"File exceeds 25 MB limit\"\n  return undefined\n}\n\nconst seedFiles: UploadItem[] = [\n  {\n    id: \"seed-1\",\n    name: \"Acme-brand-guidelines.pdf\",\n    size: 4_812_345,\n    progress: 100,\n    status: \"done\",\n  },\n  {\n    id: \"seed-2\",\n    name: \"homepage-hero@2x.png\",\n    size: 1_204_576,\n    progress: 64,\n    status: \"uploading\",\n  },\n]\n\nexport default function FileUploadBlock() {\n  const [items, setItems] = useState<UploadItem[]>(seedFiles)\n  const [isDragging, setIsDragging] = useState(false)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const dragDepth = useRef(0)\n  const hasActiveUploads = items.some((i) => i.status === \"uploading\")\n  const toastedRef = useRef<Set<string>>(\n    new Set(seedFiles.filter((f) => f.status === \"done\").map((f) => f.id))\n  )\n\n  useEffect(() => {\n    if (!hasActiveUploads) return\n    const timer = setInterval(() => {\n      setItems((prev) =>\n        prev.map((item) => {\n          if (item.status !== \"uploading\") return item\n          const next = Math.min(100, item.progress + Math.random() * 18 + 6)\n          if (next >= 100) {\n            return { ...item, progress: 100, status: \"done\" }\n          }\n          return { ...item, progress: next }\n        })\n      )\n    }, 600)\n    return () => clearInterval(timer)\n  }, [hasActiveUploads])\n\n  useEffect(() => {\n    for (const item of items) {\n      if (item.status === \"done\" && !toastedRef.current.has(item.id)) {\n        toastedRef.current.add(item.id)\n        toast.success(`${item.name} uploaded`)\n      }\n    }\n  }, [items])\n\n  const addFiles = useCallback((fileList: FileList | null) => {\n    if (!fileList) return\n    const next: UploadItem[] = Array.from(fileList).map((file) => {\n      const error = validate(file)\n      return {\n        id: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`,\n        name: file.name,\n        size: file.size,\n        progress: 0,\n        status: error ? \"error\" : \"uploading\",\n        error,\n      }\n    })\n    setItems((prev) => [...next, ...prev])\n  }, [])\n\n  const removeItem = useCallback((id: string) => {\n    setItems((prev) => prev.filter((item) => item.id !== id))\n  }, [])\n\n  const onDrop = useCallback(\n    (event: React.DragEvent<HTMLDivElement>) => {\n      event.preventDefault()\n      dragDepth.current = 0\n      setIsDragging(false)\n      addFiles(event.dataTransfer.files)\n    },\n    [addFiles]\n  )\n\n  return (\n    <section className=\"flex w-full items-center justify-center bg-muted/30 px-6 py-16 text-foreground\">\n      <Toaster />\n      <Card className=\"w-full max-w-xl\">\n        <CardHeader>\n          <CardTitle>Upload assets</CardTitle>\n          <CardDescription>\n            Drag and drop your files or browse to attach them to Acme.\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-5\">\n          <div\n            role=\"button\"\n            tabIndex={0}\n            onClick={() => inputRef.current?.click()}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault()\n                inputRef.current?.click()\n              }\n            }}\n            onDragOver={(event) => {\n              event.preventDefault()\n            }}\n            onDragEnter={(event) => {\n              event.preventDefault()\n              dragDepth.current += 1\n              setIsDragging(true)\n            }}\n            onDragLeave={(event) => {\n              event.preventDefault()\n              dragDepth.current -= 1\n              if (dragDepth.current <= 0) {\n                dragDepth.current = 0\n                setIsDragging(false)\n              }\n            }}\n            onDrop={onDrop}\n            className={cn(\n              \"flex cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed px-6 py-12 text-center transition-colors outline-none\",\n              \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n              isDragging\n                ? \"border-primary bg-primary/5\"\n                : \"border-border bg-muted/40 hover:bg-muted/60\"\n            )}\n          >\n            <input\n              ref={inputRef}\n              type=\"file\"\n              multiple\n              accept={ACCEPTED.join(\",\")}\n              className=\"sr-only\"\n              onChange={(event) => {\n                addFiles(event.target.files)\n                event.target.value = \"\"\n              }}\n            />\n            <div\n              className={cn(\n                \"flex size-12 items-center justify-center rounded-lg border transition-colors\",\n                isDragging\n                  ? \"border-primary bg-background text-primary\"\n                  : \"border-border bg-background text-muted-foreground\"\n              )}\n            >\n              <IconPlaceholder\n                lucide=\"UploadCloud\"\n                tabler=\"IconCloudUpload\"\n                hugeicons=\"CloudUploadIcon\"\n                phosphor=\"CloudArrowUp\"\n                remixicon=\"RiUploadCloud2Line\"\n                className=\"size-6\"\n                aria-hidden=\"true\"\n              />\n            </div>\n            <div className=\"flex flex-col gap-1\">\n              <p className=\"text-sm font-medium text-foreground\">\n                {isDragging\n                  ? \"Release to upload\"\n                  : \"Drag & drop files or click to 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              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={(event) => {\n                event.stopPropagation()\n                inputRef.current?.click()\n              }}\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          {items.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                    {items.length}\n                  </span>{\" \"}\n                  {items.length === 1 ? \"File\" : \"Files\"}\n                </p>\n                <button\n                  type=\"button\"\n                  onClick={() => setItems([])}\n                  className=\"text-xs font-medium text-muted-foreground transition-colors hover:text-foreground\"\n                >\n                  Clear All\n                </button>\n              </div>\n\n              <ul className=\"flex flex-col gap-2\">\n                {items.map((item) => (\n                  <li key={item.id}>\n                    <Attachment\n                      state={item.status}\n                      size=\"sm\"\n                      className=\"w-full\"\n                    >\n                      <AttachmentMedia>\n                        {item.status === \"error\" ? (\n                          <IconPlaceholder\n                            lucide=\"TriangleAlert\"\n                            tabler=\"IconAlertTriangle\"\n                            hugeicons=\"Alert01Icon\"\n                            phosphor=\"Warning\"\n                            remixicon=\"RiAlertLine\"\n                            aria-hidden=\"true\"\n                          />\n                        ) : item.status === \"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>{item.name}</AttachmentTitle>\n                        <AttachmentDescription className=\"tabular-nums\">\n                          {item.status === \"error\"\n                            ? item.error\n                            : item.status === \"done\"\n                              ? `Uploaded ${formatSize(item.size)}`\n                              : `Uploading ${Math.round(item.progress)}%`}\n                        </AttachmentDescription>\n                      </AttachmentContent>\n                      <AttachmentActions>\n                        <AttachmentAction\n                          aria-label={`Remove ${item.name}`}\n                          onClick={() => removeItem(item.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              </ul>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/file-upload-3.tsx"
    }
  ],
  "meta": {
    "height": "604px",
    "tier": "free"
  },
  "categories": [
    "file-upload"
  ],
  "type": "registry:block"
}