{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-uploader",
  "title": "File Uploader",
  "description": "A polished Retab document upload area built from the headless Dropzone primitive and File Thumbnail.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/dropzone",
    "@retab/file-thumbnail",
    "@retab/file-size-format",
    "@retab/utils"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/file-uploader.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  FileImage,\n  FileSpreadsheet,\n  FileText,\n  Upload,\n  X,\n  type LucideIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  useDropzone,\n  type DropzoneFileItem,\n  type DropzoneFileRejection,\n  type UseDropzoneProps,\n} from \"@/components/ui/dropzone\";\nimport { formatFileSize } from \"@/components/ui/file-size-format\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\nexport type FileUploaderAcceptedFileType = {\n  label: string;\n  icon: LucideIcon;\n};\n\nexport type FileUploaderProps = UseDropzoneProps &\n  Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\" | \"onDrop\"> & {\n    acceptedFileTypes?: FileUploaderAcceptedFileType[];\n    browseLabel?: React.ReactNode;\n    description?: React.ReactNode;\n    draggingLabel?: React.ReactNode;\n    showFileList?: boolean;\n    title?: React.ReactNode;\n  };\n\nconst ACCEPTED_FILE_TYPES: FileUploaderAcceptedFileType[] = [\n  { label: \"Image\", icon: FileImage },\n  { label: \"PDF\", icon: FileText },\n  { label: \"Sheet\", icon: FileSpreadsheet },\n];\n\nconst STATIC_ICON_OFFSETS = [\n  \"translate(-78%, -50%) rotate(-8deg)\",\n  \"translate(-50%, -50%)\",\n  \"translate(-22%, -50%) rotate(8deg)\",\n];\n\nexport function FileUploader({\n  accept,\n  acceptedFileTypes = ACCEPTED_FILE_TYPES,\n  browseLabel = \"Browse files\",\n  className,\n  defaultFiles,\n  description = \"PDF, DOCX, XLSX, CSV, PNG, or JPG\",\n  disabled = false,\n  draggingLabel = \"Drop to add\",\n  files,\n  maxFiles,\n  maxSize,\n  multiple = true,\n  showFileList = true,\n  title = \"Click to upload or drop files\",\n  onFilesChange,\n  onIntake,\n  ...props\n}: FileUploaderProps) {\n  const dropzone = useDropzone({\n    accept,\n    defaultFiles,\n    disabled,\n    files,\n    maxFiles,\n    maxSize,\n    multiple,\n    onFilesChange,\n    onIntake,\n  });\n  const rejectionMessage = dropzone.lastIntake.fileRejections[0]\n    ? getDropzoneRejectionMessage(dropzone.lastIntake.fileRejections[0])\n    : null;\n  const titleText = typeof title === \"string\" ? title : \"Upload files\";\n  const triggerProps = dropzone.getTriggerProps<HTMLDivElement>({\n    ...props,\n    className: cn(\n      \"relative flex min-h-64 cursor-pointer flex-col items-center justify-center gap-5 overflow-hidden rounded-lg border border-dashed bg-background px-6 py-10 text-center outline-none\",\n      \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/24\",\n      dropzone.isDragging\n        ? \"border-foreground/40 bg-accent/35\"\n        : \"border-foreground/20 hover:border-foreground/35 hover:bg-muted/35 dark:border-foreground/25 dark:hover:border-foreground/40\",\n      disabled &&\n        \"pointer-events-none cursor-not-allowed opacity-60 hover:border-foreground/20 hover:bg-background\",\n      className,\n    ),\n  });\n\n  return (\n    <div\n      {...dropzone.getRootProps({ ...triggerProps, \"data-slot\": \"dropzone\" })}\n    >\n      <FileUploaderIconCluster\n        acceptedFileTypes={acceptedFileTypes}\n        isDragging={dropzone.isDragging}\n      />\n      <div className=\"space-y-1\">\n        <div className=\"text-sm font-medium\">{title}</div>\n        <div className=\"text-muted-foreground text-xs\">{description}</div>\n        {rejectionMessage ? (\n          <div className=\"text-destructive text-xs\">{rejectionMessage}</div>\n        ) : null}\n      </div>\n      <div className=\"bg-background text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs\">\n        <Upload className=\"size-3.5\" aria-hidden />\n        <span>{dropzone.isDragging ? draggingLabel : browseLabel}</span>\n      </div>\n      {showFileList && dropzone.files.length > 0 ? (\n        <FileUploaderFileList\n          files={dropzone.files}\n          onRemoveFile={dropzone.removeFile}\n        />\n      ) : null}\n      <input\n        {...dropzone.getInputProps({\n          \"aria-label\": titleText,\n          className: \"hidden\",\n        })}\n      />\n    </div>\n  );\n}\n\nfunction FileUploaderFileList({\n  files,\n  onRemoveFile,\n}: {\n  files: DropzoneFileItem[];\n  onRemoveFile: (fileId: string) => void;\n}) {\n  return (\n    <div\n      data-slot=\"file-uploader-file-list\"\n      className=\"bg-background/80 w-full max-w-xl rounded-lg border p-3 text-left shadow-xs\"\n    >\n      <div className=\"mb-3 flex items-center justify-between gap-3\">\n        <div className=\"text-sm font-medium\">\n          {files.length} file{files.length === 1 ? \"\" : \"s\"} ready\n        </div>\n        <div className=\"text-muted-foreground text-xs\">\n          {formatFileSize(\n            files.reduce((totalSize, item) => totalSize + item.file.size, 0),\n          )}\n        </div>\n      </div>\n      <div className=\"grid grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-x-2 gap-y-4\">\n        {files.map((item) => (\n          <FileUploaderFileTile\n            key={item.id}\n            item={item}\n            onRemoveFile={onRemoveFile}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction FileUploaderFileTile({\n  item,\n  onRemoveFile,\n}: {\n  item: DropzoneFileItem;\n  onRemoveFile: (fileId: string) => void;\n}) {\n  return (\n    <div\n      data-slot=\"file-uploader-file-item\"\n      className=\"flex min-w-0 flex-col items-center gap-2\"\n    >\n      <div className=\"relative\">\n        <FileThumbnail\n          file={item.file}\n          thumbnailShape=\"square\"\n          thumbnailSize=\"lg\"\n          className=\"bg-background shrink-0 shadow-sm ring-1 ring-black/5\"\n        />\n        <button\n          type=\"button\"\n          aria-label={`Remove ${item.file.name}`}\n          className=\"bg-background text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring/24 absolute -top-1 -right-1 grid size-5 place-items-center rounded-[4px] border shadow-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none\"\n          onClick={(event) => {\n            event.preventDefault();\n            event.stopPropagation();\n            onRemoveFile(item.id);\n          }}\n          onKeyDown={(event) => {\n            event.stopPropagation();\n          }}\n        >\n          <X className=\"size-3\" aria-hidden />\n        </button>\n      </div>\n      <div className=\"max-w-full text-center\">\n        <div className=\"text-foreground line-clamp-2 text-xs leading-tight break-words\">\n          {item.file.name}\n        </div>\n        <div className=\"text-muted-foreground mt-0.5 truncate text-[0.6875rem] leading-none\">\n          {formatFileSize(item.file.size)}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction FileUploaderIconCluster({\n  acceptedFileTypes,\n  isDragging,\n}: {\n  acceptedFileTypes: FileUploaderAcceptedFileType[];\n  isDragging: boolean;\n}) {\n  const visibleTypes = acceptedFileTypes.slice(0, 3);\n\n  return (\n    <div className=\"relative h-14 w-36\" aria-hidden>\n      {visibleTypes.map((item, index) => {\n        const Icon = item.icon;\n\n        return (\n          <div\n            key={item.label}\n            className={cn(\n              \"bg-background text-muted-foreground absolute top-1/2 left-1/2 grid size-12 place-items-center rounded-lg border shadow-xs\",\n              index === 1 && \"z-10\",\n              isDragging &&\n                \"bg-popover text-foreground shadow-md shadow-black/10 dark:shadow-black/25\",\n            )}\n            style={{\n              transform:\n                visibleTypes.length === 1\n                  ? \"translate(-50%, -50%)\"\n                  : STATIC_ICON_OFFSETS[index],\n            }}\n          >\n            <Icon className=\"size-5\" />\n          </div>\n        );\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:ui",
      "target": "@ui/file-uploader.tsx"
    }
  ],
  "type": "registry:ui"
}