bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 9.26 kB
JSON
{
"name": "ui-components-dropzone",
"type": "registry:component",
"dependencies": [
"@radix-ui/react-icons"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/ui-components/DropZone.tsx",
"type": "registry:component",
"content": "\"use client\";\n\nimport { FileIcon, UpdateIcon, UploadIcon } from \"@radix-ui/react-icons\";\nimport { useCallback, useState } from \"react\";\nimport { HEIGHTS } from \"../../lib/layout-constants.js\";\nimport { cn } from \"../../lib/utils.js\";\n\nexport interface DropZoneProps {\n\tonDrop?: (files: FileList) => void;\n\tonFileSelect?: (file: File) => void;\n\taccept?: string;\n\tmultiple?: boolean;\n\tdisabled?: boolean;\n\tloading?: boolean;\n\tclassName?: string;\n\t// Visual customization\n\theight?: string | number;\n\ticon?: React.ReactNode;\n\ttitle?: string;\n\tdescription?: string;\n\tloadingText?: string;\n\tdragActiveText?: string;\n\tdragActiveDescription?: string;\n\t// File validation\n\tmaxSize?: number;\n\tonError?: (error: string) => void;\n\tsize?: \"sm\" | \"default\" | \"lg\";\n}\n\n/**\n * DropZone - Standardized drag & drop component with consistent styling\n *\n * Features:\n * - Beautiful dashed border styling with CSS variables\n * - Radix icons for consistent theming\n * - Drag states with visual feedback\n * - File validation and error handling\n * - Fully accessible with keyboard support\n * - Customizable content and styling\n *\n * @example\n * ```tsx\n * // Basic file upload\n * <DropZone\n * accept=\".json,.txt\"\n * onFileSelect={(file) => processFile(file)}\n * title=\"Import Backup\"\n * description=\"Drag & drop your backup file here\"\n * />\n *\n * // Custom data drop zone\n * <DropZone\n * icon={<TokensIcon />}\n * title=\"Add Transaction Data\"\n * description=\"Drop your transaction files\"\n * height={200}\n * />\n * ```\n */\nexport function DropZone({\n\tonDrop,\n\tonFileSelect,\n\taccept,\n\tmultiple = false,\n\tdisabled = false,\n\tloading = false,\n\tclassName = \"\",\n\theight = HEIGHTS.CARD_MIN,\n\ticon,\n\ttitle = \"Upload Files\",\n\tdescription = \"Drag & drop or click to select\",\n\tloadingText = \"Processing...\",\n\tdragActiveText = \"Drop files here\",\n\tdragActiveDescription = \"Release to upload\",\n\tmaxSize,\n\tonError,\n\tsize = \"default\",\n}: DropZoneProps) {\n\tconst [dragActive, setDragActive] = useState(false);\n\n\tconst validateFile = useCallback(\n\t\t(file: File): string | null => {\n\t\t\tif (maxSize && file.size > maxSize) {\n\t\t\t\treturn `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`;\n\t\t\t}\n\n\t\t\tif (accept) {\n\t\t\t\tconst acceptedTypes = accept.split(\",\").map((type) => type.trim());\n\t\t\t\tconst isAccepted = acceptedTypes.some((type) => {\n\t\t\t\t\tif (type.startsWith(\".\")) {\n\t\t\t\t\t\treturn file.name.toLowerCase().endsWith(type.toLowerCase());\n\t\t\t\t\t}\n\t\t\t\t\treturn file.type === type;\n\t\t\t\t});\n\n\t\t\t\tif (!isAccepted) {\n\t\t\t\t\treturn `Invalid file type. Accepted types: ${accept}`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\t\t[accept, maxSize],\n\t);\n\n\tconst handleFiles = useCallback(\n\t\t(fileList: FileList) => {\n\t\t\tif (disabled || loading) return;\n\n\t\t\tconst filesArray = Array.from(fileList);\n\n\t\t\t// Validate files\n\t\t\tfor (const file of filesArray) {\n\t\t\t\tconst error = validateFile(file);\n\t\t\t\tif (error) {\n\t\t\t\t\tonError?.(error);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle files\n\t\t\tif (onDrop) {\n\t\t\t\tonDrop(fileList);\n\t\t\t} else if (onFileSelect && filesArray.length > 0) {\n\t\t\t\tconst firstFile = filesArray[0];\n\t\t\t\tif (firstFile) {\n\t\t\t\t\tonFileSelect(firstFile);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[disabled, loading, validateFile, onDrop, onFileSelect, onError],\n\t);\n\n\tconst handleFileSelect = useCallback(\n\t\t(e: React.ChangeEvent<HTMLInputElement>) => {\n\t\t\tconst files = e.target.files;\n\t\t\tif (files) {\n\t\t\t\thandleFiles(files);\n\t\t\t}\n\t\t\t// Reset input value so same file can be selected again\n\t\t\te.target.value = \"\";\n\t\t},\n\t\t[handleFiles],\n\t);\n\n\tconst handleDrop = useCallback(\n\t\t(e: React.DragEvent) => {\n\t\t\te.preventDefault();\n\t\t\tsetDragActive(false);\n\n\t\t\tif (disabled || loading) return;\n\n\t\t\tconst files = e.dataTransfer.files;\n\t\t\tif (files) {\n\t\t\t\thandleFiles(files);\n\t\t\t}\n\t\t},\n\t\t[disabled, loading, handleFiles],\n\t);\n\n\tconst handleDragOver = useCallback((e: React.DragEvent) => {\n\t\te.preventDefault();\n\t}, []);\n\n\tconst handleDragEnter = useCallback(\n\t\t(e: React.DragEvent) => {\n\t\t\te.preventDefault();\n\t\t\tif (!disabled && !loading) {\n\t\t\t\tsetDragActive(true);\n\t\t\t}\n\t\t},\n\t\t[disabled, loading],\n\t);\n\n\tconst handleDragLeave = useCallback((e: React.DragEvent) => {\n\t\te.preventDefault();\n\t\t// Only hide drag state if leaving the entire drop zone\n\t\tif (!e.currentTarget.contains(e.relatedTarget as Node)) {\n\t\t\tsetDragActive(false);\n\t\t}\n\t}, []);\n\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent) => {\n\t\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\t\te.preventDefault();\n\t\t\t\t// Trigger file input click\n\t\t\t\tconst input = e.currentTarget.querySelector(\n\t\t\t\t\t'input[type=\"file\"]',\n\t\t\t\t) as HTMLInputElement;\n\t\t\t\tif (input && !disabled && !loading) {\n\t\t\t\t\tinput.click();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[disabled, loading],\n\t);\n\n\tconst isDisabled = disabled || loading;\n\tconst currentIcon = loading ? (\n\t\t<UpdateIcon className=\"animate-spin\" />\n\t) : (\n\t\ticon || <UploadIcon />\n\t);\n\tconst currentTitle = loading\n\t\t? loadingText\n\t\t: dragActive\n\t\t\t? dragActiveText\n\t\t\t: title;\n\tconst currentDescription = loading\n\t\t? \"\"\n\t\t: dragActive\n\t\t\t? dragActiveDescription\n\t\t\t: description;\n\n\tconst iconSizeClasses = {\n\t\tsm: \"h-8 w-8\",\n\t\tdefault: \"h-12 w-12\",\n\t\tlg: \"h-16 w-16\",\n\t};\n\n\tconst textSizeClasses = {\n\t\tsm: \"text-sm\",\n\t\tdefault: \"text-base\",\n\t\tlg: \"text-lg\",\n\t};\n\n\tconst descriptionSizeClasses = {\n\t\tsm: \"text-xs\",\n\t\tdefault: \"text-sm\",\n\t\tlg: \"text-base\",\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"relative grid place-items-center rounded-lg border-2 border-dashed transition-all duration-200\",\n\t\t\t\tdragActive\n\t\t\t\t\t? \"border-primary bg-primary/10 shadow-lg shadow-primary/25\"\n\t\t\t\t\t: \"border-muted-foreground/25 bg-muted/50 hover:border-muted-foreground/50\",\n\t\t\t\tisDisabled && \"cursor-not-allowed opacity-50\",\n\t\t\t\t!isDisabled && \"cursor-pointer\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tstyle={{\n\t\t\t\theight: typeof height === \"number\" ? `${height}px` : height,\n\t\t\t}}\n\t\t\ttabIndex={isDisabled ? -1 : 0}\n\t\t\t// biome-ignore lint/a11y/useSemanticElements: Drop zone requires complex drag/drop handling that native buttons don't support\n\t\t\trole=\"button\"\n\t\t\taria-label={`${title}. ${description}`}\n\t\t\tonDrop={handleDrop}\n\t\t\tonDragOver={handleDragOver}\n\t\t\tonDragEnter={handleDragEnter}\n\t\t\tonDragLeave={handleDragLeave}\n\t\t\tonKeyDown={handleKeyDown}\n\t\t>\n\t\t\t<input\n\t\t\t\ttype=\"file\"\n\t\t\t\taccept={accept}\n\t\t\t\tmultiple={multiple}\n\t\t\t\tonChange={handleFileSelect}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\tclassName=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed\"\n\t\t\t/>\n\n\t\t\t<div className=\"pointer-events-none flex flex-col items-center gap-3\">\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\ticonSizeClasses[size],\n\t\t\t\t\t\tdragActive || loading ? \"text-primary\" : \"text-muted-foreground\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{currentIcon}\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-col items-center gap-1\">\n\t\t\t\t\t<p\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"font-medium\",\n\t\t\t\t\t\t\ttextSizeClasses[size],\n\t\t\t\t\t\t\tdragActive || loading ? \"text-primary\" : \"text-foreground\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{currentTitle}\n\t\t\t\t\t</p>\n\t\t\t\t\t{currentDescription && (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\tdescriptionSizeClasses[size],\n\t\t\t\t\t\t\t\t\"text-muted-foreground\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{currentDescription}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\n\t\t\t\t{accept && !loading && (\n\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">{accept}</p>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\n// Preset variants for common use cases\nexport const FileDropZone = (props: Omit<DropZoneProps, \"icon\" | \"title\">) => (\n\t<DropZone\n\t\ticon={<FileIcon />}\n\t\ttitle=\"Select Files\"\n\t\tdescription=\"Drag & drop or click to browse\"\n\t\t{...props}\n\t/>\n);\n\nexport const BackupDropZone = (\n\tprops: Omit<DropZoneProps, \"icon\" | \"title\" | \"accept\">,\n) => (\n\t<DropZone\n\t\ticon={<UploadIcon />}\n\t\ttitle=\"Import Backup\"\n\t\tdescription=\"Drag & drop your backup file\"\n\t\taccept=\".json,.txt\"\n\t\t{...props}\n\t/>\n);\n",
"target": "<%- config.aliases.components %>/dropzone.tsx"
}
]
}