import { useCallback, useEffect, useRef, useState } from 'react'; interface UseFileInputOptions { onFilesSelected: (files: File[]) => void; enabled: boolean; } export function useFileInput({ onFilesSelected, enabled, }: UseFileInputOptions) { const [isDragging, setIsDragging] = useState(false); const inputRef = useRef(null); const dragCountRef = useRef(0); // Latest callback in a ref, so the effects below don't re-register when the // caller passes a new function each render. const onFilesRef = useRef(onFilesSelected); useEffect(() => { onFilesRef.current = onFilesSelected; }, [onFilesSelected]); // Hidden file input element useEffect(() => { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.style.display = 'none'; input.addEventListener('change', () => { if (input.files?.length) { onFilesRef.current(Array.from(input.files)); input.value = ''; } }); document.body.appendChild(input); inputRef.current = input; return () => { document.body.removeChild(input); inputRef.current = null; }; }, []); const openFilePicker = useCallback(() => { inputRef.current?.click(); }, []); // Clipboard paste useEffect(() => { if (!enabled) return; // Capture phase so file pastes always become attachments — ProseMirror // would otherwise inline pasted images as ephemeral blob: URLs. Mixed // clipboards (e.g. Excel/Word ship an image rendition alongside the text) // must still paste as text, so only file-only pastes are intercepted. const handlePaste = (e: ClipboardEvent) => { const data = e.clipboardData; if (!data) return; const files = Array.from(data.files); if (files.length === 0 || data.types.includes('text/plain')) return; e.preventDefault(); e.stopPropagation(); onFilesRef.current(files); }; window.addEventListener('paste', handlePaste, true); return () => window.removeEventListener('paste', handlePaste, true); }, [enabled]); // Drag and drop handlers const onDragOver = useCallback( (e: React.DragEvent) => { if (!enabled) return; e.preventDefault(); e.stopPropagation(); }, [enabled], ); const onDragEnter = useCallback( (e: React.DragEvent) => { if (!enabled) return; e.preventDefault(); e.stopPropagation(); dragCountRef.current++; if (dragCountRef.current === 1) setIsDragging(true); }, [enabled], ); const onDragLeave = useCallback( (e: React.DragEvent) => { if (!enabled) return; e.preventDefault(); e.stopPropagation(); dragCountRef.current--; if (dragCountRef.current === 0) setIsDragging(false); }, [enabled], ); const onDrop = useCallback( (e: React.DragEvent) => { if (!enabled) return; e.preventDefault(); e.stopPropagation(); dragCountRef.current = 0; setIsDragging(false); const files = Array.from(e.dataTransfer.files); if (files.length > 0) { onFilesRef.current(files); } }, [enabled], ); return { openFilePicker, isDragging, dropZoneProps: { onDragOver, onDragEnter, onDragLeave, onDrop }, }; }