import React, { useCallback, useEffect, useRef, useState } from 'react'; export interface DroppedFile { name: string; value: string; help?: string; } export type DragState = | 'idle' | 'dragging' | 'processing' | 'success' | 'error'; export function useFileDrop( onDrop: (value: DroppedFile) => void, listenOnPaste: boolean = false, ) { const [dragState, setDragState] = useState('idle'); const [error, setError] = useState(null); const [fileName, setFileName] = useState(null); const fileInputRef = useRef(null); const handleDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); const file = event.dataTransfer.files[0]; setFileName(file?.name || null); setDragState('dragging'); setError(null); }, []); const handleDragLeave = useCallback(() => { setDragState('idle'); setFileName(null); }, []); const handleFile = useCallback( (file: File) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result as string; const match = result.match(/^data:(.*?);base64,(.*)$/); if (!match) { setDragState('error'); setError(new Error('Invalid file format')); return; } const mimeType = match[1]; const base64 = match[2]; if (mimeType === 'application/json') { try { const decoded = atob(base64); JSON.parse(decoded); // Validate it's JSON onDrop({ name: file.name, value: decoded, }); setDragState('success'); setTimeout(() => setDragState('idle'), 2000); return; } catch { // this is fine, we'll fall back to base64. } } onDrop({ name: file.name, value: base64, }); setDragState('success'); setTimeout(() => setDragState('idle'), 2000); }; reader.onerror = () => { setDragState('error'); setError(new Error('Error loading file')); }; reader.readAsDataURL(file); }, [onDrop], ); const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); setDragState('processing'); const file = event.dataTransfer.files[0]; if (file) { handleFile(file); } }, [handleFile], ); const handleFileSelect = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setFileName(file.name); setDragState('processing'); handleFile(file); } }, [handleFile], ); const handlePaste = useCallback( (event: ClipboardEvent | React.ClipboardEvent) => { event.preventDefault(); setDragState('processing'); setError(null); try { const clipboardText = event.clipboardData?.getData('text'); if (clipboardText) { const droppedFile: DroppedFile = { name: 'clipboard', value: clipboardText, }; onDrop(droppedFile); setDragState('success'); setTimeout(() => setDragState('idle'), 2000); } else { setDragState('error'); setError(new Error('No text content in clipboard')); } } catch { setDragState('error'); setError(new Error('Failed to read clipboard content')); } }, [onDrop], ); useEffect(() => { if (!listenOnPaste) return; const globalPasteHandler = (event: ClipboardEvent) => { handlePaste(event); }; document.addEventListener('paste', globalPasteHandler); return () => { document.removeEventListener('paste', globalPasteHandler); }; }, [listenOnPaste, handlePaste]); const triggerFileInput = useCallback(() => { fileInputRef.current?.click(); }, []); return { dragState, error, fileName, handleDragOver, handleDragLeave, handleDrop, handlePaste, triggerFileInput, fileInputRef, handleFileSelect, }; }