{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uploadthing-dropzone",
  "title": "UploadThing Dropzone",
  "description": "Drag and drop file upload zone with progress tracking and file management. Self-contained component that works with any upload backend.",
  "dependencies": [
    "@uploadthing/react",
    "uploadthing"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/uploadthing/uploadthing-dropzone/components/elements/uploadthing-dropzone.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface UploadedFile {\n  name: string;\n  size: number;\n  type: string;\n  url: string;\n}\n\ninterface UploadThingDropzoneProps {\n  onUpload?: (files: File[]) => Promise<UploadedFile[]>;\n  onSelect?: (files: File[]) => void;\n  onProgress?: (progress: number) => void;\n  accept?: string;\n  maxFiles?: number;\n  maxSize?: number;\n  disabled?: boolean;\n  className?: string;\n}\n\nfunction UploadCloudIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242\" />\n      <path d=\"M12 12v9\" />\n      <path d=\"m16 16-4-4-4 4\" />\n    </svg>\n  );\n}\n\nfunction FileIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n      <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n    </svg>\n  );\n}\n\nfunction CopyIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n      <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n    </svg>\n  );\n}\n\nfunction ExternalLinkIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M15 3h6v6\" />\n      <path d=\"M10 14 21 3\" />\n      <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n    </svg>\n  );\n}\n\nfunction XIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M18 6 6 18\" />\n      <path d=\"m6 6 12 12\" />\n    </svg>\n  );\n}\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes === 0) return \"0 B\";\n  const k = 1024;\n  const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;\n}\n\nexport function UploadThingDropzone({\n  onUpload,\n  onSelect,\n  onProgress,\n  accept = \"image/*\",\n  maxFiles = 4,\n  maxSize = 4 * 1024 * 1024,\n  disabled = false,\n  className,\n}: UploadThingDropzoneProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isDragOver, setIsDragOver] = useState(false);\n  const [isUploading, setIsUploading] = useState(false);\n  const [progress, setProgress] = useState(0);\n  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);\n  const [error, setError] = useState<string | null>(null);\n\n  const handleFiles = useCallback(\n    async (files: File[]) => {\n      if (files.length === 0) return;\n\n      const validFiles = files.slice(0, maxFiles);\n      setError(null);\n\n      for (const file of validFiles) {\n        if (file.size > maxSize) {\n          setError(`File too large. Max size: ${formatFileSize(maxSize)}`);\n          return;\n        }\n      }\n\n      onSelect?.(validFiles);\n\n      if (onUpload) {\n        try {\n          setIsUploading(true);\n          setProgress(0);\n\n          const interval = setInterval(() => {\n            setProgress((prev) => {\n              const next = Math.min(prev + 10, 90);\n              onProgress?.(next);\n              return next;\n            });\n          }, 200);\n\n          const results = await onUpload(validFiles);\n          clearInterval(interval);\n          setProgress(100);\n          onProgress?.(100);\n          setUploadedFiles((prev) => [...prev, ...results]);\n\n          setTimeout(() => {\n            setProgress(0);\n          }, 1000);\n        } catch (err) {\n          setError(err instanceof Error ? err.message : \"Upload failed\");\n        } finally {\n          setIsUploading(false);\n        }\n      }\n    },\n    [maxFiles, maxSize, onSelect, onUpload, onProgress]\n  );\n\n  const handleDragOver = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    if (!disabled) setIsDragOver(true);\n  }, [disabled]);\n\n  const handleDragLeave = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    setIsDragOver(false);\n  }, []);\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent) => {\n      e.preventDefault();\n      setIsDragOver(false);\n      if (disabled || isUploading) return;\n\n      const files = Array.from(e.dataTransfer.files);\n      handleFiles(files);\n    },\n    [disabled, isUploading, handleFiles]\n  );\n\n  const handleFileChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const files = e.target.files ? Array.from(e.target.files) : [];\n      handleFiles(files);\n      e.target.value = \"\";\n    },\n    [handleFiles]\n  );\n\n  const handleClick = () => {\n    if (!disabled && !isUploading) {\n      inputRef.current?.click();\n    }\n  };\n\n  const removeFile = (index: number) => {\n    setUploadedFiles((prev) => prev.filter((_, i) => i !== index));\n  };\n\n  const copyUrl = (url: string) => {\n    navigator.clipboard.writeText(url);\n  };\n\n  return (\n    <div data-slot=\"uploadthing-dropzone\" className={cn(\"w-full space-y-4\", className)}>\n      <div\n        onClick={handleClick}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onDrop={handleDrop}\n        className={cn(\n          \"relative border-2 border-dashed rounded-lg p-8\",\n          \"flex flex-col items-center justify-center gap-4\",\n          \"transition-all duration-200 cursor-pointer\",\n          \"hover:border-primary/50 hover:bg-muted/30\",\n          isDragOver && \"border-primary bg-primary/5\",\n          disabled && \"opacity-50 cursor-not-allowed hover:border-border hover:bg-transparent\",\n          isUploading && \"pointer-events-none\"\n        )}\n      >\n        <div className=\"flex flex-col items-center gap-2 text-center\">\n          <UploadCloudIcon className={cn(\n            \"w-10 h-10 text-muted-foreground transition-colors\",\n            isDragOver && \"text-primary\"\n          )} />\n          \n          {isUploading ? (\n            <div className=\"w-48 space-y-2\">\n              <div className=\"w-full bg-muted rounded-full h-2 overflow-hidden\">\n                <div\n                  className=\"bg-primary h-2 rounded-full transition-all duration-300 ease-out\"\n                  style={{ width: `${progress}%` }}\n                />\n              </div>\n              <p className=\"text-sm text-muted-foreground\">{progress}% uploading...</p>\n            </div>\n          ) : (\n            <>\n              <p className=\"text-sm font-medium\">\n                {isDragOver ? \"Drop files here\" : \"Drop files here or click to browse\"}\n              </p>\n              <p className=\"text-xs text-muted-foreground\">\n                Max {maxFiles} files, up to {formatFileSize(maxSize)} each\n              </p>\n            </>\n          )}\n        </div>\n\n        <input\n          ref={inputRef}\n          type=\"file\"\n          accept={accept}\n          multiple={maxFiles > 1}\n          onChange={handleFileChange}\n          disabled={disabled || isUploading}\n          className=\"sr-only\"\n          aria-label=\"Upload files\"\n        />\n      </div>\n\n      {error && (\n        <p className=\"text-sm text-destructive\">{error}</p>\n      )}\n\n      {uploadedFiles.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center justify-between\">\n            <h4 className=\"text-sm font-medium\">Uploaded Files</h4>\n            <button\n              type=\"button\"\n              onClick={() => setUploadedFiles([])}\n              className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              Clear all\n            </button>\n          </div>\n          <div className=\"grid gap-2 max-h-48 overflow-y-auto\">\n            {uploadedFiles.map((file, index) => (\n              <div\n                key={`${file.name}-${index}`}\n                className=\"flex items-center justify-between p-3 bg-muted/50 rounded-lg border border-border\"\n              >\n                <div className=\"flex items-center gap-3 min-w-0 flex-1\">\n                  <FileIcon className=\"w-4 h-4 text-muted-foreground shrink-0\" />\n                  <div className=\"min-w-0\">\n                    <p className=\"text-sm font-medium truncate\">{file.name}</p>\n                    <p className=\"text-xs text-muted-foreground\">\n                      {formatFileSize(file.size)} • {file.type || \"unknown\"}\n                    </p>\n                  </div>\n                </div>\n                <div className=\"flex items-center gap-1 shrink-0\">\n                  <button\n                    type=\"button\"\n                    onClick={() => copyUrl(file.url)}\n                    className=\"p-1.5 hover:bg-muted rounded text-muted-foreground hover:text-foreground transition-colors\"\n                    title=\"Copy URL\"\n                  >\n                    <CopyIcon className=\"w-4 h-4\" />\n                  </button>\n                  <a\n                    href={file.url}\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"p-1.5 hover:bg-muted rounded text-muted-foreground hover:text-foreground transition-colors\"\n                    title=\"Open file\"\n                  >\n                    <ExternalLinkIcon className=\"w-4 h-4\" />\n                  </a>\n                  <button\n                    type=\"button\"\n                    onClick={() => removeFile(index)}\n                    className=\"p-1.5 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive transition-colors\"\n                    title=\"Remove\"\n                  >\n                    <XIcon className=\"w-4 h-4\" />\n                  </button>\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Drag and drop upload zone with visual feedback, progress indication, and uploaded file management. Wire up to any upload backend via the onUpload prop.",
  "categories": [
    "uploadthing"
  ],
  "type": "registry:block"
}