'use client'; import { UploadIcon } from 'lucide-react'; import type { ReactNode } from 'react'; import { createContext, useContext } from 'react'; import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone'; import { useDropzone } from 'react-dropzone'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles']; }; const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`; }; const DropzoneContext = createContext( undefined ); export type DropzoneProps = Omit & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode; }; export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props }: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( ); }; const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context; }; export type DropzoneContentProps = { children?: ReactNode; className?: string; }; const maxLabelItems = 3; export const DropzoneContent = ({ children, className, }: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return (

{src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))}

Drag and drop or click to replace

); }; export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string; }; export const DropzoneEmptyState = ({ children, className, }: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return (

Upload {maxFiles === 1 ? 'a file' : 'files'}

Drag and drop or click to upload

{caption && (

{caption}.

)}
); };