"use client"; import { CheckIcon, ChevronDownIcon, FileTextIcon, PaperclipIcon, PencilIcon, SendIcon, SparklesIcon, Trash2Icon, Undo2Icon, XIcon, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control"; import { cn } from "@/lib/utils"; export interface EmailRecipient { name: string; email: string; firstName?: string; } export interface EmailAttachment { name: string; size: string; } export interface DraftReviewProps { recipients: EmailRecipient[]; subject: string; bodies: Record; attachment?: EmailAttachment; undoMs?: number; onSend?: (draft: { to: EmailRecipient; subject: string; body: string; attachment?: EmailAttachment }) => void; className?: string; } type Phase = "draft" | "sent" | "discarded"; const spring = { type: "spring", stiffness: 420, damping: 34 } as const; const easeOut = [0.23, 1, 0.32, 1] as const; const press = "transition-[scale,background-color,box-shadow] duration-150 ease-out active:scale-[0.97]"; function fill(template: string, r: EmailRecipient) { return template.replaceAll("{name}", r.firstName ?? r.name.split(" ")[0]); } function ToneSwitch({ tones, value, onChange }: { tones: T[]; value: T; onChange: (t: T) => void }) { const refs = useRef<(HTMLButtonElement | null)[]>([]); const index = tones.indexOf(value); const onKeyDown = (e: KeyboardEvent) => { if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; e.preventDefault(); const next = (index + (e.key === "ArrowRight" ? 1 : -1) + tones.length) % tones.length; onChange(tones[next]); refs.current[next]?.focus(); }; return ( {tones.map((t, i) => { const active = t === value; return ( onChange(t)} ref={(el: HTMLButtonElement | null) => { refs.current[i] = el; }} role="radio" tabIndex={active ? 0 : -1} > {t} ); })} ); } function RecipientPicker({ recipients, value, onChange, disabled, }: { recipients: EmailRecipient[]; value: number; onChange: (i: number) => void; disabled?: boolean; }) { const reduce = useReducedMotion(); const [open, setOpen] = useState(false); const [hi, setHi] = useState(value); const listId = useId(); const current = recipients[value]; const pick = (i: number) => { onChange(i); setOpen(false); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") return setOpen(false); if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); if (!open) { setHi(value); return setOpen(true); } setHi((h) => (h + (e.key === "ArrowDown" ? 1 : -1) + recipients.length) % recipients.length); } if (open && e.key === "Enter") { e.preventDefault(); pick(hi); } }; return (
{ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setOpen(false); }} > {open && ( {recipients.map((r, i) => (
  • { e.preventDefault(); pick(i); }} onMouseEnter={() => setHi(i)} role="option" > {r.name} {r.email} {i === value && }
  • ))}
    )}
    ); } export function DraftReview({ recipients, subject: initialSubject, bodies, attachment: initialAttachment, undoMs = 5000, onSend, className, }: DraftReviewProps) { const reduce = useReducedMotion(); const tones = Object.keys(bodies) as T[]; const [to, setTo] = useState(0); const [tone, setTone] = useState(tones[0]); const [subject, setSubject] = useState(initialSubject); const [custom, setCustom] = useState(null); const [editing, setEditing] = useState(false); const [rewriting, setRewriting] = useState(false); const [attachment, setAttachment] = useState(initialAttachment); const [phase, setPhase] = useState("draft"); const [toast, setToast] = useState(null); const textRef = useRef(null); const innerRef = useRef(null); const [height, setHeight] = useState("auto"); useLayoutEffect(() => { const el = innerRef.current; if (!el) return; const ro = new ResizeObserver(() => setHeight(el.offsetHeight)); ro.observe(el); return () => ro.disconnect(); }, []); const subjectId = useId(); const generated = fill(bodies[tone], recipients[to]); const body = custom ?? generated; const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; return; } if (reduce) return; setRewriting(true); const t = setTimeout(() => setRewriting(false), 320); return () => clearTimeout(t); }, [tone, to, reduce]); useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), undoMs); return () => clearTimeout(t); }, [toast, undoMs]); useEffect(() => { const el = textRef.current; if (!editing || !el) return; el.focus(); el.setSelectionRange(el.value.length, el.value.length); }, [editing]); const rewrite = (fn: () => void) => { setCustom(null); setEditing(false); fn(); }; const finish = (kind: "sent" | "discarded") => { setEditing(false); setPhase(kind); setToast({ kind, key: Date.now() }); if (kind === "sent") onSend?.({ to: recipients[to], subject, body, attachment }); }; const undo = () => { setPhase("draft"); setToast(null); }; const reset = () => { setPhase("draft"); setToast(null); setCustom(null); setSubject(initialSubject); setAttachment(initialAttachment); }; return (
    Draft ready for review {custom !== null && phase === "draft" && ( Edited )} {phase === "draft" ? "Not sent" : phase === "sent" ? "Sent" : "Discarded"}
    {phase === "draft" ? (
    To rewrite(() => setTo(i))} recipients={recipients} value={to} />
    setSubject(e.target.value)} value={subject} />
    Tone rewrite(() => setTone(t))} tones={tones} value={tone} />
    {editing ? (