{ "name": "file-upload", "type": "registry:component", "dependencies": [ "lucide-react", "motion" ], "files": [ { "type": "registry:component", "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: File Upload\n * @version: 1.0.0\n * @date: 2025-06-26\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { UploadCloud } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport {\n type DragEvent,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype FileStatus = \"idle\" | \"dragging\" | \"uploading\" | \"error\";\n\ninterface FileError {\n message: string;\n code: string;\n}\n\ninterface FileUploadProps {\n onUploadSuccess?: (file: File) => void;\n onUploadError?: (error: FileError) => void;\n acceptedFileTypes?: string[];\n maxFileSize?: number;\n currentFile?: File | null;\n onFileRemove?: () => void;\n /** Duration in milliseconds for the upload simulation. Defaults to 2000ms (2s), 0 for no simulation */\n uploadDelay?: number;\n validateFile?: (file: File) => FileError | null;\n className?: string;\n}\n\nconst DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\nconst UPLOAD_STEP_SIZE = 5;\nconst FILE_SIZES = [\n \"Bytes\",\n \"KB\",\n \"MB\",\n \"GB\",\n \"TB\",\n \"PB\",\n \"EB\",\n \"ZB\",\n \"YB\",\n] as const;\n\nconst formatBytes = (bytes: number, decimals = 2): string => {\n if (!+bytes) return \"0 Bytes\";\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n const unit = FILE_SIZES[i] || FILE_SIZES[FILE_SIZES.length - 1];\n return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${unit}`;\n};\n\nconst UploadIllustration = () => (\n
\n \n Upload File Illustration\n \n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n \n \n
\n);\n\nconst UploadingAnimation = ({ progress }: { progress: number }) => (\n
\n \n Upload Progress Indicator\n\n \n \n \n \n \n \n\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n);\n\nexport default function FileUpload({\n onUploadSuccess = () => {},\n onUploadError = () => {},\n acceptedFileTypes = [],\n maxFileSize = DEFAULT_MAX_FILE_SIZE,\n currentFile: initialFile = null,\n onFileRemove = () => {},\n uploadDelay = 2000,\n validateFile = () => null,\n className,\n}: FileUploadProps) {\n const [file, setFile] = useState(initialFile);\n const [status, setStatus] = useState(\"idle\");\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState(null);\n const fileInputRef = useRef(null);\n const uploadIntervalRef = useRef(null);\n\n useEffect(\n () => () => {\n if (uploadIntervalRef.current) {\n clearInterval(uploadIntervalRef.current);\n }\n },\n []\n );\n\n const validateFileSize = useCallback(\n (file: File): FileError | null => {\n if (file.size > maxFileSize) {\n return {\n message: `File size exceeds ${formatBytes(maxFileSize)}`,\n code: \"FILE_TOO_LARGE\",\n };\n }\n return null;\n },\n [maxFileSize]\n );\n\n const validateFileType = useCallback(\n (file: File): FileError | null => {\n if (!acceptedFileTypes?.length) return null;\n\n const fileType = file.type.toLowerCase();\n if (\n !acceptedFileTypes.some((type) => fileType.match(type.toLowerCase()))\n ) {\n return {\n message: `File type must be ${acceptedFileTypes.join(\", \")}`,\n code: \"INVALID_FILE_TYPE\",\n };\n }\n return null;\n },\n [acceptedFileTypes]\n );\n\n const handleError = useCallback(\n (error: FileError) => {\n setError(error);\n setStatus(\"error\");\n onUploadError?.(error);\n\n setTimeout(() => {\n setError(null);\n setStatus(\"idle\");\n }, 3000);\n },\n [onUploadError]\n );\n\n const simulateUpload = useCallback(\n (uploadingFile: File) => {\n let currentProgress = 0;\n\n if (uploadIntervalRef.current) {\n clearInterval(uploadIntervalRef.current);\n }\n\n uploadIntervalRef.current = setInterval(\n () => {\n currentProgress += UPLOAD_STEP_SIZE;\n if (currentProgress >= 100) {\n if (uploadIntervalRef.current) {\n clearInterval(uploadIntervalRef.current);\n }\n setProgress(0);\n setStatus(\"idle\");\n setFile(null);\n onUploadSuccess?.(uploadingFile);\n } else {\n setStatus((prevStatus) => {\n if (prevStatus === \"uploading\") {\n setProgress(currentProgress);\n return \"uploading\";\n }\n if (uploadIntervalRef.current) {\n clearInterval(uploadIntervalRef.current);\n }\n return prevStatus;\n });\n }\n },\n uploadDelay / (100 / UPLOAD_STEP_SIZE)\n );\n },\n [onUploadSuccess, uploadDelay]\n );\n\n const handleFileSelect = useCallback(\n (selectedFile: File | null) => {\n if (!selectedFile) return;\n\n // Reset error state\n setError(null);\n\n // Validate file\n const sizeError = validateFileSize(selectedFile);\n if (sizeError) {\n handleError(sizeError);\n return;\n }\n\n const typeError = validateFileType(selectedFile);\n if (typeError) {\n handleError(typeError);\n return;\n }\n\n const customError = validateFile?.(selectedFile);\n if (customError) {\n handleError(customError);\n return;\n }\n\n setFile(selectedFile);\n setStatus(\"uploading\");\n setProgress(0);\n simulateUpload(selectedFile);\n },\n [\n simulateUpload,\n validateFileSize,\n validateFileType,\n validateFile,\n handleError,\n ]\n );\n\n const handleDragOver = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setStatus((prev) => (prev !== \"uploading\" ? \"dragging\" : prev));\n }, []);\n\n const handleDragLeave = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setStatus((prev) => (prev === \"dragging\" ? \"idle\" : prev));\n }, []);\n\n const handleDrop = useCallback(\n (e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (status === \"uploading\") return;\n setStatus(\"idle\");\n const droppedFile = e.dataTransfer.files?.[0];\n if (droppedFile) handleFileSelect(droppedFile);\n },\n [status, handleFileSelect]\n );\n\n const handleFileInputChange = useCallback(\n (e: React.ChangeEvent) => {\n const selectedFile = e.target.files?.[0];\n handleFileSelect(selectedFile || null);\n if (e.target) e.target.value = \"\";\n },\n [handleFileSelect]\n );\n\n const triggerFileInput = useCallback(() => {\n if (status === \"uploading\") return;\n fileInputRef.current?.click();\n }, [status]);\n\n const resetState = useCallback(() => {\n setFile(null);\n setStatus(\"idle\");\n setProgress(0);\n if (onFileRemove) onFileRemove();\n }, [onFileRemove]);\n\n return (\n \n
\n
\n\n
\n \n \n
\n
\n
\n
\n
\n
\n\n
\n\n
\n \n {status === \"idle\" || status === \"dragging\" ? (\n \n
\n \n
\n\n
\n

\n Drag and drop or\n

\n

\n {acceptedFileTypes?.length\n ? `${acceptedFileTypes\n .map((t) => t.split(\"/\")[1])\n .join(\", \")\n .toUpperCase()}`\n : \"SVG, PNG, JPG or GIF\"}{\" \"}\n {maxFileSize && `up to ${formatBytes(maxFileSize)}`}\n

\n
\n\n \n Upload File\n \n \n\n

\n or drag and drop your file here\n

\n\n \n \n ) : status === \"uploading\" ? (\n \n
\n \n
\n\n
\n

\n {file?.name}\n

\n
\n \n {formatBytes(file?.size || 0)}\n \n \n {Math.round(progress)}%\n \n
\n
\n\n \n Cancel\n \n \n ) : null}\n
\n
\n\n \n {error && (\n \n

\n {error.message}\n

\n \n )}\n
\n
\n
\n
\n
\n );\n}\n\nFileUpload.displayName = \"FileUpload\";\n", "path": "/components/kokonutui/file-upload.tsx", "target": "components/kokonutui/file-upload.tsx" } ] }