{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dropzone-react", "title": "Dropzone (React)", "description": "A file upload dropzone component powered by Convex storage. Supports drag and drop, file validation, upload progress, and full Convex backend.", "dependencies": [ "lucide-react", "react-dropzone", "convex@latest", "@auth/core", "@convex-dev/auth@latest" ], "registryDependencies": ["button"], "files": [ { "path": "src/registry/convex/blocks/dropzone-react/components/dropzone.tsx", "content": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport {\n AlertCircle,\n CheckCircle,\n FileAudioIcon,\n FileIcon,\n FileTextIcon,\n FileVideoIcon,\n ImageIcon,\n Loader2,\n Upload,\n X,\n} from \"lucide-react\";\nimport { useCallback, useMemo } from \"react\";\nimport { type FileError } from \"react-dropzone\";\nimport { type UseConvexUploadReturn } from \"../hooks/use-convex-upload\";\n\ninterface FileWithPreview extends File {\n preview?: string;\n errors: readonly FileError[];\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const sizes = [\"Bytes\", \"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\nfunction getFileIcon(type: string): typeof FileIcon {\n if (type.startsWith(\"image/\")) return ImageIcon;\n if (type.startsWith(\"video/\")) return FileVideoIcon;\n if (type.startsWith(\"audio/\")) return FileAudioIcon;\n if (type.startsWith(\"text/\") || type.includes(\"document\"))\n return FileTextIcon;\n return FileIcon;\n}\n\ninterface DropzoneProps {\n upload: UseConvexUploadReturn;\n className?: string;\n}\n\nexport function Dropzone({ upload, className }: DropzoneProps) {\n const {\n files,\n setFiles,\n loading,\n errors,\n successes,\n isSuccess,\n onUpload,\n maxFileSize,\n maxFiles,\n getRootProps,\n getInputProps,\n isDragActive,\n open,\n } = upload;\n\n const removeFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((f: FileWithPreview) => f.name !== fileName));\n },\n [files, setFiles],\n );\n\n const hasErrors = useMemo(() => {\n return (\n files.some((f: FileWithPreview) => f.errors.length > 0) ||\n errors.length > 0\n );\n }, [files, errors]);\n\n const canUpload = useMemo(() => {\n return files.length > 0 && !loading && !hasErrors && !isSuccess;\n }, [files.length, loading, hasErrors, isSuccess]);\n\n return (\n
\n {isDragActive\n ? \"Drop your files here\"\n : \"Drag and drop files here\"}\n
\n\n or{\" \"}\n \n
\n\n {file.name}\n
\n {isUploaded && (\n\n {formatBytes(file.size)}\n
\n {hasFileErrors && (\n\n {file.errors\n .map((e: FileError) => e.message)\n .join(\", \")}\n
\n )}\n {uploadError && (\n\n {uploadError.message}\n
\n )}\n