"use client"; import { CheckIcon, LightbulbIcon, RotateCcwIcon, SparklesIcon, Undo2Icon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface NudgeOption { id: string; label: string; title: string; detail?: string; confidence: number; reasons?: string[]; result?: string; } export interface NudgeProps { options: NudgeOption[]; undoMs?: number; onAccept?: (option: NudgeOption) => void; onUndo?: (option: NudgeOption) => void; className?: string; } const spring = { type: "spring", stiffness: 400, damping: 32 } as const; const press = "transition-[scale,background-color,box-shadow] duration-150 ease-out active:scale-[0.97]"; function level(c: number) { if (c >= 0.8) return { label: "High confidence", bar: "bg-success", text: "text-success-foreground" }; if (c >= 0.55) return { label: "Fair confidence", bar: "bg-warning", text: "text-warning-foreground" }; return { label: "Low confidence", bar: "bg-muted-foreground", text: "text-muted-foreground" }; } function RollingPercent({ value }: { value: number }) { const reduce = useReducedMotion(); const [shown, setShown] = useState(value); const from = useRef(value); useEffect(() => { if (reduce) { setShown(value); return; } const start = from.current; const t0 = performance.now(); let raf = 0; const tick = (t: number) => { const p = Math.min(1, (t - t0) / 500); const e = 1 - (1 - p) ** 3; setShown(Math.round(start + (value - start) * e)); if (p < 1) raf = requestAnimationFrame(tick); else from.current = value; }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); from.current = value; }; }, [value, reduce]); return {shown}%; } function ConfidenceMeter({ value }: { value: number }) { const reduce = useReducedMotion(); const l = level(value); const bars = 5; const filled = Math.max(1, Math.round(value * bars)); return (
{Array.from({ length: bars }, (_, i) => ( ))}
{l.label} ยท
); } export function Nudge({ options, undoMs = 5000, onAccept, onUndo, className }: NudgeProps) { const reduce = useReducedMotion(); const [index, setIndex] = useState(0); const [dir, setDir] = useState(1); const [phase, setPhase] = useState<"open" | "accepted" | "settled" | "dismissed">("open"); const current = options[index]; 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(); }, []); useEffect(() => { if (phase !== "accepted") return; const t = setTimeout(() => setPhase("settled"), undoMs); return () => clearTimeout(t); }, [phase, undoMs]); const pick = (i: number) => { if (i === index) return; setDir(i > index ? 1 : -1); setIndex(i); }; return (
{phase === "open" ? (
{index === 0 ? "Suggested" : "Alternative"}
reduce ? { opacity: 0 } : { x: d * -40, opacity: 0, filter: "blur(4px)", transition: { duration: 0.18 } }, }} >

{current.title}

{current.detail &&

{current.detail}

} {current.reasons && current.reasons.length > 0 && (
    {current.reasons.map((r, i) => ( {r} ))}
)}
{options.length > 1 && (

Options

{options.map((o, i) => { const active = i === index; return ( ); })}
)}
) : ( {phase === "dismissed" ? : }

{phase === "dismissed" ? "Suggestion dismissed" : current.result ?? "Done"}

{phase === "dismissed" ? "We'll suggest something else later." : current.title}

{phase === "accepted" ? ( ) : ( )}
)}
); }