/** * Dropzone — a large paper-textured zone that accepts PDF and images. * * States: * idle : hairline border, "Drop a document" copy, sample buttons visible * over : border deepens, corner marks pulse in * selected : filename appears, "extract" primary CTA lights up * busy : dashed border animates, ghost typewriter under filename */ import { motion, AnimatePresence } from "motion/react"; import { useCallback, useRef, useState } from "react"; import type { DocType } from "@/types"; import { SAMPLE_DOCS, loadSampleAsFile, type SampleDoc } from "@/lib/samples"; const ACCEPT = "application/pdf,image/png,image/jpeg,image/webp,image/tiff,image/bmp,text/plain,text/html,.txt,.htm,.html"; interface Props { file: File | null; docType: DocType; setFile: (f: File | null) => void; setDocType: (d: DocType) => void; onExtract: () => void; onSample: (sample: SampleDoc) => void; busy: boolean; } export function Dropzone({ file, docType, setFile, setDocType, onExtract, onSample, busy, }: Props) { const inputRef = useRef(null); const [over, setOver] = useState(false); const onFiles = useCallback( (files: FileList | null) => { if (!files || !files[0]) return; setFile(files[0]); }, [setFile] ); const onDrop = (e: React.DragEvent) => { e.preventDefault(); setOver(false); onFiles(e.dataTransfer.files); }; const pickSample = async (sample: SampleDoc) => { try { const f = await loadSampleAsFile(sample); setFile(f); setDocType(sample.docType); onSample(sample); } catch (err) { console.warn("Sample load failed", err); } }; return (
{/* --- The dropzone card ------------------------------------------- */}
{ e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={onDrop} data-cursor="focus" className="relative overflow-hidden rounded-[6px] border border-[var(--rule)] bg-[var(--surface)] transition-colors duration-500 ease-editorial" style={{ borderColor: over ? "var(--ink)" : undefined, }} > {/* Corner tick marks — appear when dragging over */}

Upload

{file ? "Ready to extract" : "Drop a document"}

{file ? ( {file.name} · {(file.size / 1024).toFixed(1)} KB ) : ( PDF or image. Up to 10 MB. We keep nothing — every extraction is stateless. )} onFiles(e.target.files)} />
{/* Progress bar during extraction */} {busy && ( )}
{/* --- Options row ------------------------------------------------- */}
{/* --- Sample buttons --------------------------------------------- */}

Or try a sample

{SAMPLE_DOCS.map((s) => ( ))}
); } /* ------------------------------------------------------------------------ */ function DocTypePicker({ value, onChange, }: { value: DocType; onChange: (v: DocType) => void; }) { const opts: { key: DocType; label: string }[] = [ { key: "receipt", label: "Receipt" }, { key: "invoice", label: "Invoice" }, { key: "filing", label: "10-K" }, ]; return (
Type {opts.map((o) => { const active = value === o.key; return ( ); })}
); } function Corners({ active }: { active: boolean }) { const stroke = "var(--ink)"; const s = 24; const off = 16; const style = { transition: "opacity 400ms cubic-bezier(0.16,1,0.3,1)" }; return (
{/* Four L-shaped corner marks */} {[ { top: off, left: off, path: `M0 ${s} L0 0 L${s} 0` }, { top: off, right: off, path: `M${-s} 0 L0 0 L0 ${s}` }, { bottom: off, left: off, path: `M0 ${-s} L0 0 L${s} 0` }, { bottom: off, right: off, path: `M${-s} 0 L0 0 L0 ${-s}` }, ].map((c, i) => ( ))}
); } function Arrow() { return ( ); }