{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropzone-block",
  "title": "Dropzone",
  "description": "A file-uploader lab proving the headless dropzone primitive across default, non-button, native-button, controlled, validation-only, custom grid, upload progress, uploader-viewer, and disabled surfaces.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/dropzone",
    "@retab/file-viewer",
    "@retab/file-size-format",
    "@retab/file-uploader",
    "@retab/file-thumbnail"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/dropzone-block.tsx",
      "content": "\"use client\";\n\nimport { DropzoneShowcase } from \"./dropzone-showcase\";\n\nexport function DropzoneBlock() {\n  return (\n    <div className=\"bg-background h-full min-h-[760px] overflow-auto p-5\">\n      <DropzoneShowcase />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-block.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-showcase.tsx",
      "content": "\"use client\";\n\nimport {\n  AvatarImageSlot,\n  CustomThumbnailGrid,\n  EvidenceTimeline,\n  MediaTranscriptQueue,\n  SpreadsheetImportCard,\n  UploadProgressQueue,\n} from \"./dropzone-file-examples\";\nimport { DefaultFileUploaderExample } from \"./dropzone-file-uploader-example\";\nimport { DropzoneFileViewerExample } from \"./dropzone-file-viewer-example\";\nimport {\n  ControlledQueue,\n  DisabledDropzone,\n  NativeButtonQueue,\n  NonButtonTrigger,\n  ValidationOnly,\n} from \"./dropzone-trigger-examples\";\nimport {\n  ComparisonPairUpload,\n  IntakeRouter,\n  PinboardDropSurface,\n  RequiredPacketSlots,\n} from \"./dropzone-workflow-examples\";\n\nexport function DropzoneShowcase() {\n  return (\n    <div className=\"mx-auto grid max-w-6xl grid-cols-12 gap-4\">\n      <DefaultFileUploaderExample className=\"col-span-12 xl:col-span-7\" />\n      <NonButtonTrigger className=\"col-span-12 md:col-span-6 xl:col-span-5\" />\n      <NativeButtonQueue className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <ControlledQueue className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <ValidationOnly className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <CustomThumbnailGrid className=\"col-span-12 xl:col-span-8\" />\n      <DropzoneFileViewerExample className=\"col-span-12 xl:col-span-8\" />\n      <MediaTranscriptQueue className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <UploadProgressQueue className=\"col-span-12 xl:col-span-8\" />\n      <AvatarImageSlot className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <SpreadsheetImportCard className=\"col-span-12 md:col-span-6 xl:col-span-4\" />\n      <EvidenceTimeline className=\"col-span-12 xl:col-span-8\" />\n      <ComparisonPairUpload className=\"col-span-12 xl:col-span-6\" />\n      <IntakeRouter className=\"col-span-12 xl:col-span-6\" />\n      <RequiredPacketSlots className=\"col-span-12 xl:col-span-7\" />\n      <PinboardDropSurface className=\"col-span-12 xl:col-span-5\" />\n      <DisabledDropzone className=\"col-span-12 xl:col-span-4\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-showcase.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"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-file-uploader-example.tsx",
      "content": "\"use client\";\n\nimport { FileUploader } from \"@/components/ui/file-uploader\";\n\nimport { type DropzoneExampleProps } from \"./dropzone-example-shared\";\n\nexport function DefaultFileUploaderExample({\n  className,\n}: DropzoneExampleProps) {\n  return (\n    <section className={className}>\n      <FileUploader\n        accept=\".pdf,.doc,.docx,.xls,.xlsx,.csv,.png,.jpg,.jpeg,application/pdf,image/png,image/jpeg,text/csv\"\n        className=\"min-h-[28rem] justify-start pt-8\"\n        description=\"PDF, DOCX, XLSX, CSV, PNG, or JPG\"\n        maxFiles={6}\n        multiple\n        title=\"Default file uploader\"\n      />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-file-uploader-example.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-file-viewer-example.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { type DropzoneExampleProps } from \"./dropzone-example-shared\";\nimport { FileIntakeViewer } from \"./dropzone-uploader-viewer\";\n\nexport function DropzoneFileViewerExample({ className }: DropzoneExampleProps) {\n  return <FileIntakeViewer className={cn(\"h-[34rem]\", className)} />;\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-file-viewer-example.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-trigger-examples.tsx",
      "content": "export { ControlledQueue } from \"./dropzone-controlled-queue\";\nexport { DisabledDropzone } from \"./dropzone-disabled-dropzone\";\nexport { NativeButtonQueue } from \"./dropzone-native-button-queue\";\nexport { NonButtonTrigger } from \"./dropzone-non-button-trigger\";\nexport { ValidationOnly } from \"./dropzone-validation-only\";\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-trigger-examples.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-controlled-queue.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone, type DropzoneFileItem } from \"@/components/ui/dropzone\";\n\nimport {\n  InlineFileRows,\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function ControlledQueue({ className }: DropzoneExampleProps) {\n  const [files, setFiles] = React.useState<DropzoneFileItem[]>([]);\n  const dropzone = useDropzone({\n    files,\n    maxFiles: 4,\n    multiple: true,\n    onFilesChange: setFiles,\n  });\n\n  return (\n    <section\n      {...dropzone.getRootProps({\n        className: cn(\n          \"rounded-lg border bg-muted/20 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-center justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Controlled queue</div>\n          <div className=\"text-muted-foreground text-xs\">\n            Parent-owned file state.\n          </div>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <button\n            className=\"text-muted-foreground hover:bg-muted h-8 rounded-md border px-2 text-xs\"\n            onClick={dropzone.clearFiles}\n            type=\"button\"\n          >\n            Clear\n          </button>\n          <button\n            {...dropzone.getTriggerProps({\n              native: true,\n              className:\n                \"inline-flex h-8 cursor-pointer items-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n            })}\n          >\n            Browse\n          </button>\n        </div>\n      </div>\n      <InlineFileRows files={files} onRemove={dropzone.removeFile} />\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-controlled-queue.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-disabled-dropzone.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\n\nimport { type DropzoneExampleProps } from \"./dropzone-example-shared\";\n\nexport function DisabledDropzone({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({ disabled: true, multiple: true });\n\n  return (\n    <section\n      {...dropzone.getRootProps(\n        dropzone.getTriggerProps({\n          \"data-slot\": \"dropzone\",\n          className: cn(\n            \"flex min-h-40 flex-col justify-center rounded-lg border border-dashed bg-muted/20 p-4 opacity-60 outline-none\",\n            className,\n          ),\n        }),\n      )}\n    >\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"text-sm font-medium\">Disabled state</div>\n      <div className=\"text-muted-foreground mt-1 text-xs\">\n        The primitive disables input, trigger focus, and drag state.\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-disabled-dropzone.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-native-button-queue.tsx",
      "content": "\"use client\";\n\nimport { Paperclip } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\n\nimport {\n  InlineFileRows,\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function NativeButtonQueue({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \"image/*,.pdf\",\n    maxFiles: 2,\n    multiple: true,\n  });\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-center justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Native button trigger</div>\n          <div className=\"text-muted-foreground text-xs\">\n            A real button uses browser button semantics.\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 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          <Paperclip className=\"size-3.5\" aria-hidden />\n          Add\n        </button>\n      </div>\n      <InlineFileRows files={dropzone.files} onRemove={dropzone.removeFile} />\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-native-button-queue.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-non-button-trigger.tsx",
      "content": "\"use client\";\n\nimport { Upload } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\n\nimport {\n  InlineFileRows,\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function NonButtonTrigger({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.csv,.txt,text/plain,text/csv,application/pdf\",\n    maxFiles: 3,\n    multiple: true,\n  });\n\n  return (\n    <section\n      {...dropzone.getRootProps({\n        className: cn(\n          \"rounded-lg border bg-muted/20 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-center gap-3\">\n        <div\n          {...dropzone.getTriggerProps({\n            className:\n              \"inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border bg-background px-3 text-sm font-medium shadow-xs outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          <Upload className=\"size-4\" aria-hidden />\n          Non-button trigger\n        </div>\n        <div className=\"min-w-0 text-sm\">\n          <div className=\"font-medium\">Controls upload</div>\n          <div className=\"text-muted-foreground truncate text-xs\">\n            {dropzone.files.length\n              ? `${dropzone.files.length} attached`\n              : \"No files attached\"}\n          </div>\n        </div>\n      </div>\n      <InlineFileRows files={dropzone.files} onRemove={dropzone.removeFile} />\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-non-button-trigger.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-validation-only.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  useDropzone,\n  type DropzoneFileRejection,\n} from \"@/components/ui/dropzone\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function ValidationOnly({ className }: DropzoneExampleProps) {\n  const [lastIntake, setLastIntake] = React.useState({\n    acceptedFiles: [] as File[],\n    fileRejections: [] as DropzoneFileRejection[],\n  });\n  const dropzone = useDropzone({\n    accept: \"application/pdf,.pdf\",\n    files: [],\n    maxSize: 100 * 1024,\n    multiple: true,\n    onFilesChange: () => {},\n    onIntake: setLastIntake,\n  });\n\n  return (\n    <section\n      {...dropzone.getRootProps(\n        dropzone.getTriggerProps({\n          \"data-slot\": \"dropzone\",\n          className: cn(\n            \"flex min-h-40 cursor-pointer flex-col justify-center rounded-lg border border-dashed bg-background p-4 transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n            dropzone.isDragging && \"border-foreground/40 bg-accent/35\",\n            className,\n          ),\n        }),\n      )}\n    >\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"text-sm font-medium\">Validation only</div>\n      <div className=\"text-muted-foreground mt-1 text-xs\">\n        PDF under 100 KB. Accepted files are reported, not stored.\n      </div>\n      <div className=\"mt-3 text-xs\">\n        Accepted:{\" \"}\n        {lastIntake.acceptedFiles.map((file) => file.name).join(\", \") || \"none\"}\n      </div>\n      <RejectionRows rejections={lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-validation-only.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-file-examples.tsx",
      "content": "export { AvatarImageSlot } from \"./dropzone-avatar-image-slot\";\nexport { CustomThumbnailGrid } from \"./dropzone-custom-thumbnail-grid\";\nexport { EvidenceTimeline } from \"./dropzone-evidence-timeline\";\nexport { MediaTranscriptQueue } from \"./dropzone-media-transcript-queue\";\nexport { SpreadsheetImportCard } from \"./dropzone-spreadsheet-import-card\";\nexport { UploadProgressQueue } from \"./dropzone-upload-progress-queue\";\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-file-examples.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-avatar-image-slot.tsx",
      "content": "\"use client\";\n\nimport { ImagePlus } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function AvatarImageSlot({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \"image/*,.png,.jpg,.jpeg,.webp\",\n    maxFiles: 1,\n  });\n  const selectedFile = dropzone.files[0];\n\n  return (\n    <section\n      {...dropzone.getRootProps({\n        className: cn(\n          \"rounded-lg border bg-muted/20 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-center justify-between gap-3\">\n        <div>\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <ImagePlus className=\"text-muted-foreground size-4\" aria-hidden />\n            Avatar image slot\n          </div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            One image, replaceable by design.\n          </div>\n        </div>\n        {selectedFile ? (\n          <button\n            className=\"bg-background hover:bg-muted h-8 rounded-md border px-3 text-xs font-medium\"\n            onClick={dropzone.clearFiles}\n            type=\"button\"\n          >\n            Remove\n          </button>\n        ) : null}\n      </div>\n      <div\n        {...dropzone.getTriggerProps({\n          className:\n            \"mt-4 grid min-h-44 cursor-pointer place-items-center rounded-md border border-dashed bg-background p-4 text-center outline-none transition-colors hover:bg-muted/40 focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n        })}\n      >\n        {selectedFile ? (\n          <div className=\"min-w-0\">\n            <FileThumbnail\n              file={selectedFile.file}\n              previewAspectRatio={1}\n              className=\"mx-auto size-24 rounded-full\"\n            />\n            <div className=\"mt-3 line-clamp-1 max-w-48 text-sm font-medium\">\n              {selectedFile.file.name}\n            </div>\n            <div className=\"text-muted-foreground text-xs\">\n              Click or drop to replace.\n            </div>\n          </div>\n        ) : (\n          <div>\n            <ImagePlus className=\"text-muted-foreground mx-auto size-8\" />\n            <div className=\"mt-3 text-sm font-medium\">Drop profile image</div>\n            <div className=\"text-muted-foreground mt-1 text-xs\">\n              PNG, JPG, or WebP.\n            </div>\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-avatar-image-slot.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-custom-thumbnail-grid.tsx",
      "content": "\"use client\";\n\nimport { Upload } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function CustomThumbnailGrid({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.png,.jpg,.jpeg,image/*,application/pdf\",\n    multiple: true,\n  });\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      <div className=\"mb-3 flex flex-wrap items-center justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Custom thumbnail grid</div>\n          <div className=\"text-muted-foreground text-xs\">\n            Direct useDropzone composition with FileThumbnail.\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 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          <Upload className=\"size-3.5\" aria-hidden />\n          Select files\n        </button>\n      </div>\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"bg-muted/20 grid min-h-36 grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-3 rounded-md border border-dashed p-3\">\n        {dropzone.files.length ? (\n          dropzone.files.map((item) => (\n            <div key={item.id} className=\"min-w-0 text-center\">\n              <FileThumbnail\n                file={item.file}\n                previewAspectRatio={1}\n                className=\"bg-background mx-auto size-16 shadow-sm\"\n              />\n              <div className=\"mt-2 line-clamp-2 text-xs leading-tight break-words\">\n                {item.file.name}\n              </div>\n            </div>\n          ))\n        ) : (\n          <div className=\"text-muted-foreground col-span-full grid place-items-center text-xs\">\n            Drop PDFs or images here.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-custom-thumbnail-grid.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-evidence-timeline.tsx",
      "content": "\"use client\";\n\nimport { Clock3, Paperclip, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function EvidenceTimeline({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.png,.jpg,.jpeg,image/*,application/pdf\",\n    maxFiles: 6,\n    multiple: true,\n  });\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 flex-wrap items-center justify-between gap-3\">\n        <div>\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <Clock3 className=\"text-muted-foreground size-4\" aria-hidden />\n            Evidence timeline\n          </div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            Files become ordered events inside a custom surface.\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 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          <Paperclip className=\"size-3.5\" aria-hidden />\n          Add evidence\n        </button>\n      </div>\n      <div className=\"bg-muted/20 mt-4 grid min-h-44 auto-rows-min content-start items-start gap-3 rounded-md border border-dashed p-3 md:grid-cols-2\">\n        {dropzone.files.length ? (\n          dropzone.files.map((item) => (\n            <div\n              key={item.id}\n              className=\"bg-background flex h-16 min-w-0 items-center gap-3 rounded-md border p-2\"\n            >\n              <FileThumbnail\n                file={item.file}\n                thumbnailShape=\"square\"\n                thumbnailSize=\"md\"\n                className=\"shrink-0\"\n              />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"truncate text-sm font-medium\">\n                  {item.file.name}\n                </div>\n                <div className=\"text-muted-foreground text-xs\">\n                  {formatFileSize(item.file.size)}\n                </div>\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-7 shrink-0 place-items-center rounded-md\"\n                onClick={() => dropzone.removeFile(item.id)}\n              >\n                <X className=\"size-4\" aria-hidden />\n              </button>\n            </div>\n          ))\n        ) : (\n          <div\n            {...dropzone.getTriggerProps({\n              className:\n                \"col-span-full grid min-h-36 cursor-pointer place-items-center rounded-md bg-background text-center text-xs text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n            })}\n          >\n            Drop PDFs or images to build a case timeline.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-evidence-timeline.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-media-transcript-queue.tsx",
      "content": "\"use client\";\n\nimport { FileAudio, Upload, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function MediaTranscriptQueue({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \"audio/*,video/*,.mp3,.wav,.m4a,.mp4,.mov\",\n    maxFiles: 5,\n    multiple: true,\n  });\n  const totalSize = dropzone.files.reduce(\n    (total, item) => total + item.file.size,\n    0,\n  );\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            <FileAudio className=\"text-muted-foreground size-4\" aria-hidden />\n            Audio transcript queue\n          </div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            Audio or video files become transcript jobs.\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          <Upload className=\"size-3.5\" aria-hidden />\n          Add media\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            <div className=\"text-muted-foreground flex items-center justify-between px-1 text-xs\">\n              <span>{dropzone.files.length} jobs queued</span>\n              <span>{formatFileSize(totalSize)}</span>\n            </div>\n            {dropzone.files.map((item) => (\n              <div\n                key={item.id}\n                className=\"bg-background flex min-w-0 items-center gap-2 rounded-md border p-2 text-xs\"\n              >\n                <FileThumbnail\n                  file={item.file}\n                  thumbnailShape=\"square\"\n                  thumbnailSize=\"sm\"\n                  className=\"shrink-0\"\n                />\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"truncate font-medium\">{item.file.name}</div>\n                  <div className=\"text-muted-foreground\">\n                    {formatFileSize(item.file.size)} · queued\n                  </div>\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-6 shrink-0 place-items-center rounded-[4px]\"\n                  onClick={() => dropzone.removeFile(item.id)}\n                >\n                  <X className=\"size-3.5\" aria-hidden />\n                </button>\n              </div>\n            ))}\n          </div>\n        ) : (\n          <div className=\"text-muted-foreground grid h-32 place-items-center text-center text-xs\">\n            Drop interview recordings here.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-media-transcript-queue.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-spreadsheet-import-card.tsx",
      "content": "\"use client\";\n\nimport { Table2, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function SpreadsheetImportCard({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept:\n      \".csv,.xls,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n    maxFiles: 1,\n  });\n  const selectedFile = dropzone.files[0];\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>\n          <div className=\"flex items-center gap-2 text-sm font-medium\">\n            <Table2 className=\"text-muted-foreground size-4\" aria-hidden />\n            Spreadsheet mapper\n          </div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            A single sheet feeds a mapping workflow.\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 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          Choose\n        </button>\n      </div>\n      <div className=\"bg-muted/20 mt-4 rounded-md border border-dashed p-3\">\n        {selectedFile ? (\n          <div className=\"space-y-3\">\n            <div className=\"flex min-w-0 items-center gap-3\">\n              <FileThumbnail\n                file={selectedFile.file}\n                previewAspectRatio={1}\n                className=\"bg-background size-12 shrink-0\"\n              />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"truncate text-sm font-medium\">\n                  {selectedFile.file.name}\n                </div>\n                <div className=\"text-muted-foreground text-xs\">\n                  {formatFileSize(selectedFile.file.size)}\n                </div>\n              </div>\n              <button\n                type=\"button\"\n                aria-label={`Remove ${selectedFile.file.name}`}\n                className=\"text-muted-foreground hover:bg-background hover:text-foreground grid size-7 shrink-0 place-items-center rounded-md\"\n                onClick={dropzone.clearFiles}\n              >\n                <X className=\"size-4\" aria-hidden />\n              </button>\n            </div>\n            <div className=\"grid grid-cols-3 gap-2 text-xs\">\n              {[\"Name\", \"Email\", \"Amount\"].map((column) => (\n                <div\n                  key={column}\n                  className=\"bg-background rounded-md border px-2 py-1.5 text-center font-medium\"\n                >\n                  {column}\n                </div>\n              ))}\n            </div>\n          </div>\n        ) : (\n          <div\n            {...dropzone.getTriggerProps({\n              className:\n                \"grid min-h-32 cursor-pointer place-items-center rounded-md bg-background text-center text-xs text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n            })}\n          >\n            Drop CSV or XLSX to preview columns.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-spreadsheet-import-card.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-uploader-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type {\n  DropzoneFileItem,\n  DropzoneIntake,\n} from \"@/components/ui/dropzone\";\nimport { ViewerBody } from \"@/components/ui/viewer\";\n\nimport {\n  FileIntakeViewerDropTarget,\n  FileIntakeViewerHeader,\n  FileIntakeViewerProvider,\n  FileIntakeViewerRoot,\n  FileIntakeViewerSidebar,\n  FileIntakeViewerSurface,\n} from \"./dropzone-uploader-viewer-parts\";\n\nexport type FileIntakeViewerProps = {\n  accept?: string;\n  className?: string;\n  defaultFiles?: DropzoneFileItem[];\n  disabled?: boolean;\n  files?: DropzoneFileItem[];\n  maxSize?: number;\n  onFilesChange?: (files: DropzoneFileItem[]) => void;\n  onIntake?: (intake: DropzoneIntake) => void;\n};\n\nexport function FileIntakeViewer({\n  accept,\n  className,\n  defaultFiles,\n  disabled,\n  files,\n  maxSize,\n  onFilesChange,\n  onIntake,\n}: FileIntakeViewerProps) {\n  return (\n    <FileIntakeViewerProvider\n      accept={accept}\n      defaultFiles={defaultFiles}\n      disabled={disabled}\n      files={files}\n      maxSize={maxSize}\n      onFilesChange={onFilesChange}\n      onIntake={onIntake}\n    >\n      <FileIntakeViewerDropTarget>\n        <FileIntakeViewerRoot className={className}>\n          <FileIntakeViewerHeader />\n          <ViewerBody className=\"flex-col md:flex-row\">\n            <FileIntakeViewerSidebar />\n            <FileIntakeViewerSurface />\n          </ViewerBody>\n        </FileIntakeViewerRoot>\n      </FileIntakeViewerDropTarget>\n    </FileIntakeViewerProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-uploader-viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-uploader-viewer-parts.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Upload, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { blobSource } from \"@/lib/viewer-resource\";\nimport type { BlobViewerSource } from \"@/lib/viewer-source\";\nimport {\n  useDropzone,\n  type DropzoneFileItem,\n  type DropzoneFileRejection,\n  type DropzoneIntake,\n  type UseDropzoneReturn,\n} from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\nimport { FileViewerPreview } from \"@/components/ui/file-viewer\";\nimport {\n  ViewerHeader,\n  ViewerRoot,\n  ViewerSidebar,\n  ViewerSidebarTrigger,\n  ViewerSurface,\n} from \"@/components/ui/viewer\";\n\nconst DEFAULT_FILE_INTAKE_VIEWER_ACCEPT =\n  \".pdf,.png,.jpg,.jpeg,.csv,.txt,.md,.json,application/pdf,image/*,text/*,text/csv,application/json\";\n\nexport interface FileIntakeViewerProviderProps {\n  accept?: string;\n  defaultFiles?: DropzoneFileItem[];\n  disabled?: boolean;\n  files?: DropzoneFileItem[];\n  maxSize?: number;\n  onFilesChange?: (files: DropzoneFileItem[]) => void;\n  onIntake?: (intake: DropzoneIntake) => void;\n  children: React.ReactNode;\n}\n\ntype FileIntakeViewerModel = {\n  canClear: boolean;\n  hasFile: boolean;\n  isDragging: boolean;\n  rejection: FileIntakeViewerRejection | null;\n  selectedFile: DropzoneFileItem | null;\n  selectedFileSummary: FileIntakeSummary | null;\n  viewerSource: BlobViewerSource | null;\n};\n\ntype FileIntakeSummary = {\n  file: File;\n  fileName: string;\n  fileSizeLabel: string;\n  fileTypeLabel: string;\n};\n\nexport type FileIntakeViewerRejection = {\n  title: string;\n  description: string;\n};\n\ntype FileIntakeViewerActions = {\n  clearFile: () => void;\n  getRootDropProps: UseDropzoneReturn[\"getRootProps\"];\n  getFileInputProps: UseDropzoneReturn[\"getInputProps\"];\n  getUploadButtonProps: UseDropzoneReturn[\"getTriggerProps\"];\n  getEmptySurfaceProps: UseDropzoneReturn[\"getTriggerProps\"];\n};\n\ntype FileIntakeViewerContextValue = {\n  actions: FileIntakeViewerActions;\n  model: FileIntakeViewerModel;\n};\n\ntype FileIntakeViewerDropTargetState = {\n  getFileInputProps: FileIntakeViewerActions[\"getFileInputProps\"];\n  getRootDropProps: FileIntakeViewerActions[\"getRootDropProps\"];\n  isDragging: boolean;\n};\n\ntype FileIntakeViewerHeaderState = {\n  canClear: boolean;\n  clearFile: FileIntakeViewerActions[\"clearFile\"];\n  getUploadButtonProps: FileIntakeViewerActions[\"getUploadButtonProps\"];\n  selectedFileSummary: FileIntakeSummary | null;\n};\n\ntype FileIntakeViewerSidebarState = {\n  getUploadButtonProps: FileIntakeViewerActions[\"getUploadButtonProps\"];\n  selectedFileSummary: FileIntakeSummary | null;\n};\n\nexport type FileIntakeViewerSurfaceState = {\n  getEmptySurfaceProps: UseDropzoneReturn[\"getTriggerProps\"];\n  rejection: FileIntakeViewerRejection | null;\n  viewerSource: BlobViewerSource | null;\n};\n\nconst FileIntakeViewerContext =\n  React.createContext<FileIntakeViewerContextValue | null>(null);\n\nfunction useFileIntakeViewerContext(): FileIntakeViewerContextValue {\n  const context = React.useContext(FileIntakeViewerContext);\n  if (!context) {\n    throw new Error(\n      \"useFileIntakeViewer must be used within FileIntakeViewerProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction useFileIntakeViewerDropTarget(): FileIntakeViewerDropTargetState {\n  const { actions, model } = useFileIntakeViewerContext();\n  return {\n    getFileInputProps: actions.getFileInputProps,\n    getRootDropProps: actions.getRootDropProps,\n    isDragging: model.isDragging,\n  };\n}\n\nfunction useFileIntakeViewerHeader(): FileIntakeViewerHeaderState {\n  const { actions, model } = useFileIntakeViewerContext();\n  return {\n    canClear: model.canClear,\n    clearFile: actions.clearFile,\n    getUploadButtonProps: actions.getUploadButtonProps,\n    selectedFileSummary: model.selectedFileSummary,\n  };\n}\n\nfunction useFileIntakeViewerSidebar(): FileIntakeViewerSidebarState {\n  const { actions, model } = useFileIntakeViewerContext();\n  return {\n    getUploadButtonProps: actions.getUploadButtonProps,\n    selectedFileSummary: model.selectedFileSummary,\n  };\n}\n\nexport function useFileIntakeViewerSurface(): FileIntakeViewerSurfaceState {\n  const { actions, model } = useFileIntakeViewerContext();\n  return {\n    getEmptySurfaceProps: actions.getEmptySurfaceProps,\n    rejection: model.rejection,\n    viewerSource: model.viewerSource,\n  };\n}\n\nexport function FileIntakeViewerProvider({\n  accept = DEFAULT_FILE_INTAKE_VIEWER_ACCEPT,\n  defaultFiles,\n  disabled,\n  files,\n  maxSize,\n  onFilesChange,\n  onIntake,\n  children,\n}: FileIntakeViewerProviderProps) {\n  const dropzone = useDropzone({\n    accept,\n    defaultFiles,\n    disabled,\n    files,\n    maxFiles: 1,\n    maxSize,\n    multiple: false,\n    onFilesChange,\n    onIntake,\n  });\n  const { clearFiles, getInputProps, getRootProps, getTriggerProps } = dropzone;\n  const model = React.useMemo<FileIntakeViewerModel>(\n    () =>\n      createFileIntakeViewerModel({\n        files: dropzone.files,\n        isDisabled: dropzone.isDisabled,\n        isDragging: dropzone.isDragging,\n        lastIntake: dropzone.lastIntake,\n      }),\n    [\n      dropzone.files,\n      dropzone.isDisabled,\n      dropzone.isDragging,\n      dropzone.lastIntake,\n    ],\n  );\n  const actions = React.useMemo<FileIntakeViewerActions>(\n    () => ({\n      clearFile: clearFiles,\n      getRootDropProps: getRootProps,\n      getFileInputProps: getInputProps,\n      getUploadButtonProps: (props) =>\n        getTriggerProps({ ...props, native: true }),\n      getEmptySurfaceProps: getTriggerProps,\n    }),\n    [clearFiles, getInputProps, getRootProps, getTriggerProps],\n  );\n  const value = React.useMemo<FileIntakeViewerContextValue>(\n    () => ({\n      actions,\n      model,\n    }),\n    [actions, model],\n  );\n\n  return (\n    <FileIntakeViewerContext.Provider value={value}>\n      {children}\n    </FileIntakeViewerContext.Provider>\n  );\n}\n\nexport function FileIntakeViewerDropTarget({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  const { getFileInputProps, getRootDropProps } =\n    useFileIntakeViewerDropTarget();\n\n  return (\n    <section\n      {...getRootDropProps({\n        className: cn(\"group/file-intake-drop contents\", className),\n      })}\n    >\n      <input {...getFileInputProps({ className: \"hidden\" })} />\n      {children}\n    </section>\n  );\n}\n\nexport function FileIntakeViewerRoot({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <ViewerRoot\n      defaultOpen\n      mode=\"inline\"\n      className={cn(\n        \"bg-background text-foreground min-h-[30rem] rounded-lg border transition-colors\",\n        \"group-data-[dragging]/file-intake-drop:border-foreground/40 group-data-[dragging]/file-intake-drop:bg-accent/35\",\n        className,\n      )}\n    >\n      {children}\n    </ViewerRoot>\n  );\n}\n\nexport function FileIntakeViewerHeader() {\n  const { canClear, clearFile, getUploadButtonProps, selectedFileSummary } =\n    useFileIntakeViewerHeader();\n\n  return (\n    <ViewerHeader className=\"flex flex-wrap items-center justify-between gap-3 px-4 py-3\">\n      <div className=\"flex min-w-0 items-center gap-2\">\n        <ViewerSidebarTrigger />\n        <div className=\"min-w-0\">\n          <div className=\"text-sm font-medium\">File preview</div>\n          {selectedFileSummary ? (\n            <div className=\"text-muted-foreground mt-1 truncate text-xs\">\n              {selectedFileSummary.fileName}\n            </div>\n          ) : null}\n        </div>\n      </div>\n      <div className=\"flex items-center gap-2\">\n        {selectedFileSummary && canClear ? (\n          <button\n            type=\"button\"\n            className=\"bg-background text-muted-foreground hover:bg-muted hover:text-foreground grid size-8 place-items-center rounded-md border\"\n            aria-label={`Remove ${selectedFileSummary.fileName}`}\n            onClick={clearFile}\n          >\n            <X className=\"size-4\" aria-hidden />\n          </button>\n        ) : null}\n        <button\n          {...getUploadButtonProps({\n            \"aria-label\": selectedFileSummary\n              ? `Replace ${selectedFileSummary.fileName}`\n              : \"Upload file\",\n            className:\n              \"inline-flex h-8 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          <Upload className=\"size-3.5\" aria-hidden />\n          {selectedFileSummary ? \"Replace\" : \"Upload file\"}\n        </button>\n      </div>\n    </ViewerHeader>\n  );\n}\n\nexport function FileIntakeViewerSidebar() {\n  const { getUploadButtonProps, selectedFileSummary } =\n    useFileIntakeViewerSidebar();\n\n  return (\n    <ViewerSidebar\n      aria-label=\"Selected file\"\n      width=\"12rem\"\n      className=\"bg-background border-b p-3 md:border-r md:border-b-0\"\n    >\n      {selectedFileSummary ? (\n        <FileIntakeViewerFileCard fileSummary={selectedFileSummary} />\n      ) : (\n        <FileIntakeViewerNoFile />\n      )}\n      {!selectedFileSummary ? (\n        <button\n          {...getUploadButtonProps({\n            \"aria-label\": \"Upload file\",\n            className:\n              \"mt-4 inline-flex h-8 w-fit cursor-pointer items-center gap-2 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          <Upload className=\"size-3.5\" aria-hidden />\n          Upload file\n        </button>\n      ) : null}\n    </ViewerSidebar>\n  );\n}\n\nexport function FileIntakeViewerSurface() {\n  const { getEmptySurfaceProps, rejection, viewerSource } =\n    useFileIntakeViewerSurface();\n\n  return (\n    <ViewerSurface className=\"min-h-[24rem]\">\n      {viewerSource ? (\n        <FileViewerPreview\n          source={viewerSource}\n          className=\"size-full min-h-0\"\n        />\n      ) : (\n        <FileIntakeViewerEmptyState\n          getEmptySurfaceProps={getEmptySurfaceProps}\n          rejection={rejection}\n        />\n      )}\n    </ViewerSurface>\n  );\n}\n\nfunction FileIntakeViewerFileCard({\n  fileSummary,\n}: {\n  fileSummary: FileIntakeSummary;\n}) {\n  return (\n    <div className=\"space-y-3\">\n      <FileThumbnail\n        file={fileSummary.file}\n        thumbnailShape=\"square\"\n        thumbnailSize=\"xl\"\n        className=\"bg-background shadow-sm\"\n      />\n      <div className=\"min-w-0\">\n        <div className=\"line-clamp-3 text-sm leading-snug font-medium break-words\">\n          {fileSummary.fileName}\n        </div>\n        <div className=\"text-muted-foreground mt-1 text-xs\">\n          {fileSummary.fileSizeLabel}\n        </div>\n      </div>\n      <div className=\"bg-background text-muted-foreground rounded-md border p-2 text-xs\">\n        {fileSummary.fileTypeLabel}\n      </div>\n    </div>\n  );\n}\n\nfunction FileIntakeViewerNoFile() {\n  return (\n    <div>\n      <div className=\"text-sm font-medium\">No file selected</div>\n      <div className=\"text-muted-foreground mt-1 text-xs\">\n        PDF, image, CSV, text, Markdown, or JSON.\n      </div>\n    </div>\n  );\n}\n\nfunction FileIntakeViewerEmptyState({\n  getEmptySurfaceProps,\n  rejection,\n}: {\n  getEmptySurfaceProps: FileIntakeViewerActions[\"getEmptySurfaceProps\"];\n  rejection: FileIntakeViewerRejection | null;\n}) {\n  return (\n    <div\n      {...getEmptySurfaceProps({\n        \"aria-label\": \"Upload file\",\n        className:\n          \"grid h-full min-h-[26rem] cursor-pointer place-items-center rounded-md border border-dashed bg-background p-6 text-center outline-none transition-colors hover:bg-muted/30 focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n      })}\n    >\n      <div>\n        <div className=\"bg-muted/30 text-muted-foreground mx-auto grid size-12 place-items-center rounded-md border\">\n          <Upload className=\"size-5\" aria-hidden />\n        </div>\n        <div className=\"mt-4 text-sm font-medium\">Upload file</div>\n        <div className=\"text-muted-foreground mt-1 text-xs\">\n          Drop a file here to open it in the viewer.\n        </div>\n        {rejection ? (\n          <div className=\"border-destructive/30 bg-destructive/5 text-destructive mt-4 rounded-md border px-3 py-2 text-xs\">\n            <div className=\"font-medium\">{rejection.title}</div>\n            <div className=\"text-destructive/80 mt-1\">\n              {rejection.description}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nfunction getSelectedFileIntakeFile(files: DropzoneFileItem[]) {\n  return files[0] ?? null;\n}\n\nfunction createFileIntakeSummary(\n  fileItem: DropzoneFileItem | null,\n): FileIntakeSummary | null {\n  if (!fileItem) return null;\n\n  return {\n    file: fileItem.file,\n    fileName: fileItem.file.name,\n    fileSizeLabel: formatFileSize(fileItem.file.size),\n    fileTypeLabel: fileItem.file.type || \"Unknown type\",\n  };\n}\n\nfunction createFileIntakeViewerSource(\n  fileItem: DropzoneFileItem,\n): BlobViewerSource {\n  return blobSource(fileItem.file, {\n    fileName: fileItem.file.name,\n    identityKey: fileItem.id,\n    mimeType: fileItem.file.type || undefined,\n  });\n}\n\nfunction createFileIntakeViewerModel(\n  dropzone: Pick<\n    UseDropzoneReturn,\n    \"files\" | \"isDragging\" | \"isDisabled\" | \"lastIntake\"\n  >,\n): FileIntakeViewerModel {\n  const selectedFile = getSelectedFileIntakeFile(dropzone.files);\n  const selectedFileSummary = createFileIntakeSummary(selectedFile);\n  const viewerSource = selectedFile\n    ? createFileIntakeViewerSource(selectedFile)\n    : null;\n\n  return {\n    canClear: selectedFile !== null && !dropzone.isDisabled,\n    hasFile: selectedFile !== null,\n    isDragging: dropzone.isDragging,\n    rejection: createFileIntakeViewerRejection(dropzone.lastIntake),\n    selectedFile,\n    selectedFileSummary,\n    viewerSource,\n  };\n}\n\nfunction createFileIntakeViewerRejection(\n  intake: DropzoneIntake,\n): FileIntakeViewerRejection | null {\n  if (intake.acceptedFiles.length > 0 || intake.fileRejections.length === 0) {\n    return null;\n  }\n\n  return describeFileIntakeRejection(intake.fileRejections[0]);\n}\n\nfunction describeFileIntakeRejection(\n  rejection: DropzoneFileRejection,\n): FileIntakeViewerRejection {\n  if (rejection.reason === \"file-invalid-type\") {\n    return {\n      title: \"Unsupported file type\",\n      description: `${rejection.file.name} cannot be opened here.`,\n    };\n  }\n\n  if (rejection.reason === \"file-too-large\") {\n    return {\n      title: \"File is too large\",\n      description: `${rejection.file.name} must be ${formatFileSize(\n        rejection.maxSize,\n      )} or smaller.`,\n    };\n  }\n\n  return {\n    title: \"Only one file can be previewed\",\n    description: `${rejection.file.name} was not added.`,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-uploader-viewer-parts.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/viewer.tsx",
      "content": "\"use client\";\n\nexport { ViewerBody } from \"./viewer-body\";\nexport type { ViewerSidebarTriggerProps } from \"./viewer-chrome\";\nexport {\n  ViewerFrame,\n  ViewerHeader,\n  ViewerSidebarTrigger,\n} from \"./viewer-chrome\";\nexport {\n  ViewerRoot,\n  useOptionalViewerSidebar,\n  useViewerSidebar,\n} from \"./viewer-root\";\nexport { ViewerSidebar } from \"./viewer-sidebar\";\nexport { ViewerSurface, ViewerViewport } from \"./viewer-surface\";\nexport type {\n  ViewerBodyProps,\n  ViewerDataAttributes,\n  ViewerFrameProps,\n  ViewerHeaderProps,\n  ViewerRootProps,\n  ViewerSidebarCollapsible,\n  ViewerSidebarStateValue,\n  ViewerSidebarMode,\n  ViewerSidebarProps,\n  ViewerSidebarRequestedMode,\n  ViewerSidebarSide,\n  ViewerSidebarSlotNames,\n  ViewerSidebarState,\n  ViewerStateAttributeNamespace,\n  ViewerStateAttributeSlot,\n  ViewerStateAttributeValues,\n  ViewerSurfaceProps,\n  ViewerViewportProps,\n} from \"./viewer-types\";\n",
      "type": "registry:ui",
      "target": "@ui/viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-workflow-examples.tsx",
      "content": "export { ComparisonPairUpload } from \"./dropzone-comparison-pair-upload\";\nexport { IntakeRouter } from \"./dropzone-intake-router\";\nexport { PinboardDropSurface } from \"./dropzone-pinboard-drop-surface\";\nexport { RequiredPacketSlots } from \"./dropzone-required-packet-slots\";\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-workflow-examples.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-comparison-pair-upload.tsx",
      "content": "\"use client\";\n\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function ComparisonPairUpload({ className }: DropzoneExampleProps) {\n  return (\n    <section className={cn(\"bg-background rounded-lg border p-4\", className)}>\n      <div className=\"mb-4 flex items-start justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Comparison pair</div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            Two independent dropzones model original versus revision.\n          </div>\n        </div>\n        <div className=\"bg-muted/30 text-muted-foreground rounded-full border px-2 py-1 text-xs\">\n          2 slots\n        </div>\n      </div>\n      <div className=\"grid gap-3 md:grid-cols-2\">\n        <ComparisonSlot label=\"Original\" />\n        <ComparisonSlot label=\"Revision\" />\n      </div>\n    </section>\n  );\n}\n\nfunction ComparisonSlot({ label }: { label: string }) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.doc,.docx,application/pdf\",\n    maxFiles: 1,\n  });\n  const selectedFile = dropzone.files[0];\n\n  return (\n    <div\n      {...dropzone.getRootProps({\n        className: cn(\n          \"rounded-md border border-dashed bg-muted/20 p-3 transition-colors\",\n          dropzone.isDragging && \"border-foreground/40 bg-accent/35\",\n        ),\n      })}\n    >\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"mb-3 flex items-center justify-between gap-2\">\n        <div className=\"text-sm font-medium\">{label}</div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-7 cursor-pointer items-center rounded-md border bg-background px-2 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          {selectedFile ? \"Replace\" : \"Choose\"}\n        </button>\n      </div>\n      {selectedFile ? (\n        <div className=\"bg-background flex min-w-0 items-center gap-3 rounded-md border p-2\">\n          <FileThumbnail\n            file={selectedFile.file}\n            thumbnailShape=\"square\"\n            thumbnailSize=\"md\"\n            className=\"shrink-0\"\n          />\n          <div className=\"min-w-0 flex-1\">\n            <div className=\"truncate text-sm font-medium\">\n              {selectedFile.file.name}\n            </div>\n            <div className=\"text-muted-foreground text-xs\">\n              {formatFileSize(selectedFile.file.size)}\n            </div>\n          </div>\n          <button\n            type=\"button\"\n            aria-label={`Remove ${selectedFile.file.name}`}\n            className=\"text-muted-foreground hover:bg-muted hover:text-foreground grid size-7 shrink-0 place-items-center rounded-md\"\n            onClick={dropzone.clearFiles}\n          >\n            <X className=\"size-4\" aria-hidden />\n          </button>\n        </div>\n      ) : (\n        <div\n          {...dropzone.getTriggerProps({\n            className:\n              \"grid min-h-24 cursor-pointer place-items-center rounded-md bg-background text-center text-xs text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          Drop {label.toLowerCase()} document.\n        </div>\n      )}\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-comparison-pair-upload.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-intake-router.tsx",
      "content": "\"use client\";\n\nimport { Upload, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone, type DropzoneFileItem } from \"@/components/ui/dropzone\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function IntakeRouter({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept:\n      \".pdf,.doc,.docx,.csv,.xls,.xlsx,.png,.jpg,.jpeg,image/*,application/pdf,text/csv\",\n    maxFiles: 12,\n    multiple: true,\n  });\n  const groups = getRoutedFiles(dropzone.files);\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=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Intake router</div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            One target, derived lanes by file type.\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 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          <Upload className=\"size-3.5\" aria-hidden />\n          Add batch\n        </button>\n      </div>\n      <div className=\"grid gap-3 md:grid-cols-3\">\n        <RoutedLane\n          label=\"Documents\"\n          files={groups.documents}\n          onRemove={dropzone.removeFile}\n        />\n        <RoutedLane\n          label=\"Images\"\n          files={groups.images}\n          onRemove={dropzone.removeFile}\n        />\n        <RoutedLane\n          label=\"Tables\"\n          files={groups.tables}\n          onRemove={dropzone.removeFile}\n        />\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n\nfunction getRoutedFiles(files: DropzoneFileItem[]) {\n  const groups = {\n    documents: [] as DropzoneFileItem[],\n    images: [] as DropzoneFileItem[],\n    tables: [] as DropzoneFileItem[],\n  };\n\n  for (const item of files) {\n    const fileName = item.file.name.toLowerCase();\n    const fileType = item.file.type;\n\n    if (\n      fileType.startsWith(\"image/\") ||\n      /\\.(png|jpe?g|gif|webp|heic)$/.test(fileName)\n    ) {\n      groups.images.push(item);\n    } else if (\n      fileType.includes(\"spreadsheet\") ||\n      fileType === \"text/csv\" ||\n      /\\.(csv|xls|xlsx)$/.test(fileName)\n    ) {\n      groups.tables.push(item);\n    } else {\n      groups.documents.push(item);\n    }\n  }\n\n  return groups;\n}\n\nfunction RoutedLane({\n  files,\n  label,\n  onRemove,\n}: {\n  files: DropzoneFileItem[];\n  label: string;\n  onRemove: (fileId: string) => void;\n}) {\n  return (\n    <div className=\"bg-muted/20 min-h-44 rounded-md border border-dashed p-2\">\n      <div className=\"mb-2 flex items-center justify-between text-xs\">\n        <span className=\"font-medium\">{label}</span>\n        <span className=\"text-muted-foreground\">{files.length}</span>\n      </div>\n      {files.length ? (\n        <div className=\"space-y-2\">\n          {files.slice(0, 3).map((item) => (\n            <div\n              key={item.id}\n              className=\"bg-background flex min-w-0 items-center gap-2 rounded-md border p-2\"\n            >\n              <FileThumbnail\n                file={item.file}\n                thumbnailShape=\"square\"\n                thumbnailSize=\"xs\"\n                className=\"shrink-0\"\n              />\n              <div className=\"min-w-0 flex-1 truncate text-xs font-medium\">\n                {item.file.name}\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-6 shrink-0 place-items-center rounded-md\"\n                onClick={() => onRemove(item.id)}\n              >\n                <X className=\"size-3.5\" aria-hidden />\n              </button>\n            </div>\n          ))}\n          {files.length > 3 ? (\n            <div className=\"text-muted-foreground text-center text-xs\">\n              +{files.length - 3} more\n            </div>\n          ) : null}\n        </div>\n      ) : (\n        <div className=\"text-muted-foreground grid h-32 place-items-center text-center text-xs\">\n          No {label.toLowerCase()} yet.\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-intake-router.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-pinboard-drop-surface.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function PinboardDropSurface({ className }: DropzoneExampleProps) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.png,.jpg,.jpeg,image/*,application/pdf\",\n    maxFiles: 8,\n    multiple: true,\n  });\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=\"mb-4 flex items-center justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Pinboard drop surface</div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            The whole canvas is the trigger.\n          </div>\n        </div>\n        <button\n          {...dropzone.getTriggerProps({\n            native: true,\n            className:\n              \"inline-flex h-8 cursor-pointer items-center 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          Pin files\n        </button>\n      </div>\n      <div\n        {...dropzone.getTriggerProps({\n          className:\n            \"grid min-h-72 cursor-pointer grid-cols-2 content-start gap-3 rounded-md border border-dashed bg-muted/20 p-3 outline-none transition-colors hover:bg-muted/30 focus-visible:ring-[3px] focus-visible:ring-ring/24 sm:grid-cols-3\",\n        })}\n      >\n        {dropzone.files.length ? (\n          dropzone.files.map((item, index) => (\n            <div\n              key={item.id}\n              className={cn(\n                \"bg-background min-w-0 rounded-md border p-2 text-center shadow-xs\",\n                index % 2 === 0 && \"translate-y-2\",\n                index % 3 === 0 && \"-rotate-1\",\n                index % 3 === 1 && \"rotate-1\",\n              )}\n            >\n              <FileThumbnail\n                file={item.file}\n                previewAspectRatio={1}\n                className=\"mx-auto size-14\"\n              />\n              <div className=\"mt-2 line-clamp-2 text-xs leading-tight break-words\">\n                {item.file.name}\n              </div>\n            </div>\n          ))\n        ) : (\n          <div className=\"text-muted-foreground col-span-full grid min-h-60 place-items-center text-center text-xs\">\n            Drop files to pin them onto the canvas.\n          </div>\n        )}\n      </div>\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-pinboard-drop-surface.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/dropzone-required-packet-slots.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useDropzone } from \"@/components/ui/dropzone\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nimport {\n  RejectionRows,\n  type DropzoneExampleProps,\n} from \"./dropzone-example-shared\";\n\nexport function RequiredPacketSlots({ className }: DropzoneExampleProps) {\n  return (\n    <section className={cn(\"bg-background rounded-lg border p-4\", className)}>\n      <div className=\"mb-4 flex items-start justify-between gap-3\">\n        <div>\n          <div className=\"text-sm font-medium\">Required packet</div>\n          <div className=\"text-muted-foreground mt-1 text-xs\">\n            Slot-level dropzones for checklist-driven uploads.\n          </div>\n        </div>\n        <div className=\"bg-muted/30 text-muted-foreground rounded-full border px-2 py-1 text-xs\">\n          checklist\n        </div>\n      </div>\n      <div className=\"grid gap-3 md:grid-cols-3\">\n        {[\"Identity proof\", \"Bank statement\", \"Board approval\"].map((label) => (\n          <PacketSlot key={label} label={label} />\n        ))}\n      </div>\n    </section>\n  );\n}\n\nfunction PacketSlot({ label }: { label: string }) {\n  const dropzone = useDropzone({\n    accept: \".pdf,.png,.jpg,.jpeg,image/*,application/pdf\",\n    maxFiles: 1,\n  });\n  const selectedFile = dropzone.files[0];\n\n  return (\n    <div\n      {...dropzone.getRootProps({\n        className: cn(\n          \"min-h-44 rounded-md border border-dashed bg-muted/20 p-3 transition-colors\",\n          dropzone.isDragging && \"border-foreground/40 bg-accent/35\",\n        ),\n      })}\n    >\n      <input {...dropzone.getInputProps({ className: \"hidden\" })} />\n      <div className=\"mb-3 flex items-center justify-between gap-2\">\n        <div className=\"text-xs font-medium\">{label}</div>\n        <div\n          className={cn(\n            \"rounded-full px-2 py-0.5 text-[11px]\",\n            selectedFile\n              ? \"bg-foreground text-background\"\n              : \"bg-background text-muted-foreground\",\n          )}\n        >\n          {selectedFile ? \"done\" : \"open\"}\n        </div>\n      </div>\n      {selectedFile ? (\n        <div className=\"text-center\">\n          <FileThumbnail\n            file={selectedFile.file}\n            thumbnailShape=\"square\"\n            thumbnailSize=\"lg\"\n            className=\"bg-background mx-auto\"\n          />\n          <div className=\"mt-2 line-clamp-2 text-xs font-medium break-words\">\n            {selectedFile.file.name}\n          </div>\n          <button\n            type=\"button\"\n            className=\"bg-background text-muted-foreground hover:bg-muted hover:text-foreground mt-3 h-7 rounded-md border px-2 text-xs\"\n            onClick={dropzone.clearFiles}\n          >\n            Clear\n          </button>\n        </div>\n      ) : (\n        <div\n          {...dropzone.getTriggerProps({\n            className:\n              \"grid h-28 cursor-pointer place-items-center rounded-md bg-background text-center text-xs text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n          })}\n        >\n          Drop required file.\n        </div>\n      )}\n      <RejectionRows rejections={dropzone.lastIntake.fileRejections} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/dropzone-required-packet-slots.tsx"
    },
    {
      "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"
    }
  ],
  "categories": [
    "dropzone"
  ],
  "type": "registry:block"
}