import * as React from 'react'; import { Upload, X, FileIcon, AlertCircle } from 'lucide-react'; import { cn } from '../../shared/utils'; import { Button } from '../button'; import { useFileUpload } from './use-file-upload'; interface FileUploadProps extends Omit< React.InputHTMLAttributes, 'onChange' | 'onError' | 'size' > { onFilesChange?: (files: File[]) => void; maxFiles?: number; maxSize?: number; // in bytes showPreview?: boolean; /** Called when files are rejected due to size or count limits. */ onError?: (rejectedFiles: File[], reason: 'size' | 'count') => void; /** * Size variant — `compact` uses a horizontal layout with smaller icons and * tighter spacing, for dense forms. * @default "default" */ size?: 'default' | 'compact'; /** Label for the clickable upload action. @default "Click to upload" */ uploadLabel?: string; /** Label for the drag-and-drop hint, shown after `uploadLabel`. @default "or drag and drop" */ dragAndDropLabel?: string; /** File-count hint shown when `maxFiles > 1`. `{count}` is replaced with `maxFiles`. @default "Up to {count} files" */ multipleFilesLabel?: string; /** File-count hint shown when `maxFiles` is 1. @default "1 file" */ singleFileLabel?: string; /** Max-size hint. `{size}` is replaced with the limit in MB. @default "Max {size}MB" */ maxSizeLabel?: string; /** * Error shown when one or more files exceed `maxSize`. `{count}` and `{size}` * are replaced with the rejected file count and the limit in MB. * @default "{count} file(s) exceed the {size}MB limit and were not added." */ sizeErrorLabel?: string; /** * Error shown when more files are selected than `maxFiles` allows. `{max}` * and `{count}` are replaced with the file limit and the rejected count. * @default "Only {max} file(s) allowed. {count} file(s) were not added." */ countErrorLabel?: string; } /** * Drag-and-drop file input with preview list. * * @description * Manages selected file state and provides a standardized, accessible interface * for file uploads. Supports click-to-browse and drag-and-drop. Shows a preview * list of selected files with per-file remove buttons. * * Headless logic is available via `useFileUpload` for custom drop-zone UIs. * * @ai-rules * 1. Use `onFilesChange` to capture the selected files array in your form state. * 2. `maxFiles` defaults to 1 (single file). Set to a higher number for multi-upload. * 3. `maxSize` is in bytes (default: 5MB). Files exceeding the limit trigger `onError` and show an inline error. * 4. Set `showPreview={false}` if you manage the file list externally. * 5. Use `onError` to handle rejected files (e.g., show a toast notification). * 6. Use `size="compact"` for dense forms; override any label prop (e.g. `uploadLabel`) to localize the UI text. */ const dropzoneClasses = { default: 'flex-col items-center justify-center p-8', compact: 'flex-row items-center gap-3 p-3 text-left', }; const uploadIconClasses = { default: 'mb-4 h-10 w-10', compact: 'h-5 w-5 shrink-0', }; const previewContainerClasses = { default: 'mt-4 space-y-2', compact: 'mt-2 space-y-1', }; const previewRowClasses = { default: 'p-3', compact: 'p-2', }; const previewIconClasses = { default: 'h-5 w-5', compact: 'h-4 w-4', }; const FileUpload = React.forwardRef( ( { className, onFilesChange, maxFiles = 1, maxSize = 5 * 1024 * 1024, // 5MB default showPreview = true, onError, accept, disabled, size = 'default', uploadLabel = 'Click to upload', dragAndDropLabel = 'or drag and drop', multipleFilesLabel = 'Up to {count} files', singleFileLabel = '1 file', maxSizeLabel = 'Max {size}MB', sizeErrorLabel, countErrorLabel, ...props }, ref ) => { const { files, dragActive, errorMessage, inputRef, handleDrag, handleDrop, handleChange, removeFile, openFileDialog, } = useFileUpload({ maxFiles, maxSize, onFilesChange, onError, disabled, sizeErrorLabel, countErrorLabel, }); const limitMB = (maxSize / 1024 / 1024).toFixed(0); const filesCountLabel = maxFiles > 1 ? multipleFilesLabel.replace('{count}', String(maxFiles)) : singleFileLabel; return (

{uploadLabel} {dragAndDropLabel}

{filesCountLabel} • {maxSizeLabel.replace('{size}', limitMB)}

1} accept={accept} disabled={disabled} />
{errorMessage && (
{errorMessage}
)} {showPreview && files.length > 0 && (
{files.map((file, index) => (

{file.name}

{(file.size / 1024).toFixed(2)} KB

))}
)}
); } ); FileUpload.displayName = 'FileUpload'; export { FileUpload }; export type { FileUploadProps };