import { FileImage, Upload } from "lucide-react"; import { type DragEvent, useCallback, useState } from "react"; import { cn } from "@/lib/utils"; const IMAGE_EXTENSIONS = new Set([ "jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "avif", "tiff", "tif", "ico", "heic", "heif", "hif", "jxl", "apng", "dng", "cr2", "cr3", "nef", "nrw", "arw", "orf", "rw2", "raf", "pef", "3fr", "iiq", "srw", "x3f", "rwl", "gpr", "fff", "mrw", "mef", "kdc", "dcr", "erf", "ptx", "tga", "psd", "exr", "hdr", "svgz", "jp2", "j2k", "j2c", "jpc", "jpf", "jpx", "qoi", "eps", "epsf", "dds", "cur", "dpx", "cin", "fits", "fit", "fts", "pbm", "pgm", "ppm", "pnm", "pam", "pfm", ]); export function isImageFile(file: File): boolean { if (file.type.startsWith("image/")) return true; const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; return IMAGE_EXTENSIONS.has(ext); } interface DropzoneProps { onFiles?: (files: File[]) => void; accept?: string; multiple?: boolean; /** Files that have already been dropped (for showing count & list). */ currentFiles?: File[]; /** Use a smaller layout that fits constrained containers like the pipeline preview. */ compact?: boolean; } // Browsers may not map certain formats (HEIC, JXL, RAW, etc.) to image/* in file pickers. // Append explicit extensions so they are selectable. function expandAccept(accept?: string): string | undefined { if (!accept?.includes("image/*")) return accept; return `${accept},.avif,.heic,.heif,.hif,.jxl,.ico,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.j2c,.jpc,.jpf,.jpx,.qoi,.eps,.epsf,.dds,.cur,.apng,.dpx,.cin,.fits,.fit,.fts,.ppm,.pgm,.pbm,.pnm,.pam,.pfm`; } export function Dropzone({ onFiles, accept, multiple = true, currentFiles = [], compact = false, }: DropzoneProps) { const resolvedAccept = expandAccept(accept); const [isDragging, setIsDragging] = useState(false); const handleDrag = useCallback((e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") setIsDragging(true); else if (e.type === "dragleave") setIsDragging(false); }, []); const handleDrop = useCallback( (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files).filter(isImageFile); if (files.length > 0) onFiles?.(files); }, [onFiles], ); const handleClick = () => { const input = document.createElement("input"); input.type = "file"; input.multiple = multiple; if (resolvedAccept) input.accept = resolvedAccept; input.onchange = (e) => { const files = Array.from((e.target as HTMLInputElement).files || []); if (files.length > 0) onFiles?.(files); }; input.click(); }; const hasMultipleFiles = currentFiles.length > 1; return (
SnapOtter

Drop files here or click the upload button

{/* Show file count badge and list when multiple files are dropped */} {hasMultipleFiles && (
{currentFiles.length} files selected
{currentFiles.map((f) => (
{f.name} {(f.size / 1024).toFixed(0)} KB
))}
)}
); }