{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropzone-upload-progress-queue",
  "title": "Upload Progress Queue",
  "description": "A dropzone-backed upload queue with per-file loading bars keyed by the admitted file ids.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/dropzone",
    "@retab/file-size-format",
    "@retab/utils",
    "@retab/use-keyed-mount-effect",
    "@retab/use-mount-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/dropzone-upload-progress-queue.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckCircle2, FileText, UploadCloud, X } from \"lucide-react\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\ntype UploadState = {\n  progress: number;\n  status: \"uploading\" | \"done\";\n};\n\n/**\n * Dropzone owns file intake; the upload transport is the consumer's job. This\n * example simulates a per-file upload and renders its progress. Real code would\n * swap the interval for `fetch`/`XMLHttpRequest` with an `onprogress` handler.\n */\nexport function UploadProgressQueue({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({ maxFiles: 5, multiple: true });\n  const [uploads, setUploads] = React.useState<Record<string, UploadState>>({});\n  const timers = React.useRef<Record<string, ReturnType<typeof setInterval>>>(\n    {},\n  );\n  const dropzoneFilesKey = React.useMemo(\n    () => dropzone.files.map((item) => item.id).join(\"\\0\"),\n    [dropzone.files],\n  );\n\n  // Start a simulated upload for every file the dropzone admits that we have\n  // not seen yet. The dropzone owns the canonical file id, so we key off it.\n  useKeyedMountEffect(dropzoneFilesKey, () => {\n    dropzone.files.forEach((item) => {\n      if (timers.current[item.id] !== undefined) return;\n\n      setUploads((prev) =>\n        prev[item.id]\n          ? prev\n          : { ...prev, [item.id]: { progress: 0, status: \"uploading\" } },\n      );\n\n      timers.current[item.id] = setInterval(() => {\n        setUploads((prev) => {\n          const current = prev[item.id];\n          if (!current || current.status === \"done\") return prev;\n\n          const progress = Math.min(\n            100,\n            current.progress + 9 + Math.random() * 11,\n          );\n          if (progress >= 100) {\n            clearInterval(timers.current[item.id]);\n            delete timers.current[item.id];\n            return { ...prev, [item.id]: { progress: 100, status: \"done\" } };\n          }\n          return { ...prev, [item.id]: { progress, status: \"uploading\" } };\n        });\n      }, 280);\n    });\n  });\n\n  useMountEffect(() => {\n    const pending = timers.current;\n    return () => {\n      Object.values(pending).forEach((timer) => clearInterval(timer));\n    };\n  });\n\n  const handleRemove = (id: string) => {\n    if (timers.current[id] !== undefined) {\n      clearInterval(timers.current[id]);\n      delete timers.current[id];\n    }\n    setUploads((prev) => {\n      const next = { ...prev };\n      delete next[id];\n      return next;\n    });\n    dropzone.removeFile(id);\n  };\n\n  const uploading = dropzone.files.filter(\n    (item) => uploads[item.id]?.status !== \"done\",\n  ).length;\n\n  return (\n    <section\n      {...dropzone.getRootProps({\n        className: cn(\n          \"rounded-lg border bg-background p-4 transition-colors\",\n          dropzone.isDragging && \"border-foreground/40 bg-accent/35\",\n          className,\n        ),\n      })}\n    >\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"min-w-0\">\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <UploadCloud className=\"text-muted-foreground size-4\" aria-hidden />\n            Upload queue\n          </div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            {uploading > 0\n              ? `Uploading ${uploading} file${uploading === 1 ? \"\" : \"s\"}…`\n              : \"Intake into React state, then upload.\"}\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 shrink-0 cursor-pointer items-center gap-2 rounded-md border bg-background px-3 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          <UploadCloud className=\"size-3.5\" aria-hidden />\n          Add files\n        </button>\n      </div>\n      <div className=\"bg-muted/20 mt-4 min-h-36 rounded-md border border-dashed p-2\">\n        {dropzone.files.length ? (\n          <div className=\"space-y-2\">\n            {dropzone.files.map((item) => {\n              const upload = uploads[item.id];\n              const progress = upload?.progress ?? 0;\n              const done = upload?.status === \"done\";\n              return (\n                <div\n                  key={item.id}\n                  className=\"bg-background flex min-w-0 items-center gap-3 rounded-md border p-2 text-xs\"\n                >\n                  <FileText\n                    className=\"text-muted-foreground size-4 shrink-0\"\n                    aria-hidden\n                  />\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"flex items-center justify-between gap-2\">\n                      <span className=\"truncate font-medium\">\n                        {item.file.name}\n                      </span>\n                      <span className=\"text-muted-foreground shrink-0\">\n                        {done ? \"Done\" : `${Math.round(progress)}%`}\n                      </span>\n                    </div>\n                    <div\n                      role=\"progressbar\"\n                      aria-valuemin={0}\n                      aria-valuemax={100}\n                      aria-valuenow={Math.round(progress)}\n                      aria-label={`Uploading ${item.file.name}`}\n                      className=\"bg-muted mt-1.5 h-1.5 w-full overflow-hidden rounded-full\"\n                    >\n                      <div\n                        className={cn(\n                          \"h-full rounded-full transition-[width] duration-300 ease-out\",\n                          done ? \"bg-emerald-500\" : \"bg-foreground/70\",\n                        )}\n                        style={{ width: `${progress}%` }}\n                      />\n                    </div>\n                    <div className=\"text-muted-foreground mt-1\">\n                      {formatFileSize(item.file.size)}\n                    </div>\n                  </div>\n                  {done ? (\n                    <CheckCircle2\n                      className=\"size-4 shrink-0 text-emerald-500\"\n                      aria-hidden\n                    />\n                  ) : null}\n                  <button\n                    type=\"button\"\n                    aria-label={`Remove ${item.file.name}`}\n                    className=\"text-muted-foreground hover:bg-muted hover:text-foreground grid size-6 shrink-0 place-items-center rounded-[4px]\"\n                    onClick={() => handleRemove(item.id)}\n                  >\n                    <X className=\"size-3.5\" aria-hidden />\n                  </button>\n                </div>\n              );\n            })}\n          </div>\n        ) : (\n          <div className=\"text-muted-foreground grid h-32 place-items-center text-center text-xs\">\n            Drop files to start uploading.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-upload-progress-queue.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-example-shared.tsx",
      "content": "\"use client\";\n\nimport { FileText, X } from \"lucide-react\";\n\nimport type {\n  DropzoneFileItem,\n  DropzoneFileRejection,\n} from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\n\nexport type DropzoneExampleProps = {\n  className?: string;\n};\n\nexport function InlineFileRows({\n  files,\n  onRemove,\n}: {\n  files: DropzoneFileItem[];\n  onRemove: (fileId: string) => void;\n}) {\n  if (files.length === 0) return null;\n\n  return (\n    <div className=\"mt-3 space-y-1\">\n      {files.map((item) => (\n        <div\n          key={item.id}\n          className=\"bg-background flex min-w-0 items-center gap-2 rounded-md border px-2 py-1.5 text-xs\"\n        >\n          <FileText className=\"text-muted-foreground size-3.5 shrink-0\" />\n          <div className=\"min-w-0 flex-1 truncate\">{item.file.name}</div>\n          <div className=\"text-muted-foreground shrink-0\">\n            {formatFileSize(item.file.size)}\n          </div>\n          <button\n            type=\"button\"\n            aria-label={`Remove ${item.file.name}`}\n            className=\"text-muted-foreground hover:bg-muted hover:text-foreground grid size-5 shrink-0 place-items-center rounded-[4px]\"\n            onClick={() => onRemove(item.id)}\n          >\n            <X className=\"size-3\" aria-hidden />\n          </button>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function RejectionRows({\n  rejections,\n}: {\n  rejections: DropzoneFileRejection[];\n}) {\n  if (rejections.length === 0) return null;\n\n  return (\n    <div className=\"text-destructive mt-3 space-y-1 text-xs\">\n      {rejections.map((rejection) => (\n        <div key={`${rejection.file.name}-${rejection.reason}`}>\n          {rejection.file.name}: {getDropzoneRejectionMessage(rejection)}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction getDropzoneRejectionMessage(rejection: DropzoneFileRejection): string {\n  if (rejection.reason === \"file-invalid-type\") {\n    return \"This file type is not supported here.\";\n  }\n  if (rejection.reason === \"file-too-large\") {\n    return `File must be ${formatFileSize(rejection.maxSize)} or smaller.`;\n  }\n  if (rejection.reason === \"custom\") {\n    return rejection.code;\n  }\n  return rejection.maxFiles === 1\n    ? \"Only one file can be selected.\"\n    : `Only ${rejection.maxFiles} files can be selected.`;\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-example-shared.tsx"
    }
  ],
  "categories": [
    "dropzone"
  ],
  "type": "registry:block"
}