"use client"; // Adapted from Kokonut UI (@dorianbaffier), MIT. See THIRD_PARTY_NOTICES.md. import { useId, useRef, useState, type ComponentProps } from "react"; export interface FileUploadProps extends Omit< ComponentProps<"div">, "onChange" > { label: string; accept?: string; maxSize?: number; disabled?: boolean; file?: File | null; onFileChange?: (file: File | null) => void; validateFile?: (file: File) => string | undefined; name?: string; } /** Selects a file; the consuming app owns upload/network/progress behavior. */ export function FileUpload({ label, accept = "", maxSize = 5 * 1024 * 1024, disabled = false, file, onFileChange, validateFile, name, className = "", onDragOver, onDragLeave, onDrop, ...props }: FileUploadProps) { const [internal, setInternal] = useState(null); const [error, setError] = useState(""); const [dragging, setDragging] = useState(false); const selected = file === undefined ? internal : file; const id = useId(); const input = useRef(null); const change = (value: File | null) => { if (file === undefined) setInternal(value); onFileChange?.(value); }; const select = (value: File | undefined) => { if (!value || disabled) return; const types = accept .split(",") .map((type) => type.trim().toLowerCase()) .filter(Boolean); const valid = types.length === 0 || types.some((type) => type.startsWith(".") ? value.name.toLowerCase().endsWith(type) : type.endsWith("/*") ? value.type.toLowerCase().startsWith(type.slice(0, -1)) : value.type.toLowerCase() === type, ); const message = value.size > maxSize ? `File size must be at most ${maxSize} bytes.` : !valid ? `File type must match ${accept}.` : validateFile?.(value); if (message) { setError(message); if (input.current) input.current.value = ""; return; } setError(""); change(value); }; return (
{ onDragOver?.(event); if (event.defaultPrevented) return; event.preventDefault(); if (!disabled) setDragging(true); }} onDragLeave={(event) => { onDragLeave?.(event); if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDragging(false); }} onDrop={(event) => { onDrop?.(event); if (event.defaultPrevented) return; event.preventDefault(); setDragging(false); select(event.dataTransfer.files[0]); }} > { select(event.currentTarget.files?.[0]); }} /> {selected && (
{selected.name}
)} {error && ( )}
); }