"use client"; import { ChevronDown, ListTodo } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ReactNode, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, } from "react"; import { ActionSwapRollText } from "@/components/motion/action-swap-roll"; import { AgentDisclosure } from "@/components/agents/agent-disclosure"; import { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP, } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type TodoItemStatus = | "pending" | "in-progress" | "completed" | "cancelled"; export interface TodoItem { id: string; title: ReactNode; status?: TodoItemStatus; progress?: number; detail?: ReactNode; } export interface TodoListProps { items: TodoItem[]; title?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; collapseOnComplete?: boolean; maxHeight?: number; className?: string; } function statusLabel(status: TodoItemStatus) { if (status === "in-progress") return "In progress"; if (status === "completed") return "Completed"; if (status === "cancelled") return "Cancelled"; return "Pending"; } function TodoHeaderIcon({ complete }: { complete: boolean }) { const reduce = useReducedMotion() ?? false; return ( ); } function TodoStatusIcon({ status, progress, }: { status: TodoItemStatus; progress?: number; }) { const reduce = useReducedMotion() ?? false; const normalizedProgress = progress === undefined ? 0.68 : Math.min(100, Math.max(0, progress)) / 100; return ( ); } export function TodoList({ items, title = "To-dos", open, defaultOpen = true, onOpenChange, collapseOnComplete = true, maxHeight = 248, className, }: TodoListProps) { const reduce = useReducedMotion() ?? false; const baseId = useId(); const triggerId = `${baseId}-trigger`; const contentId = `${baseId}-content`; const viewportRef = useRef(null); const previousComplete = useRef(false); const [internalOpen, setInternalOpen] = useState(defaultOpen); const currentOpen = open ?? internalOpen; const completed = items.filter((item) => item.status === "completed").length; const allComplete = items.length > 0 && completed === items.length; const itemCount = items.length; const setOpen = useCallback( (next: boolean) => { if (open === undefined) setInternalOpen(next); onOpenChange?.(next); }, [onOpenChange, open], ); useEffect(() => { if (previousComplete.current && !allComplete) { setOpen(true); } if (!previousComplete.current && allComplete && collapseOnComplete) { setOpen(false); } previousComplete.current = allComplete; }, [allComplete, collapseOnComplete, setOpen]); useLayoutEffect(() => { const viewport = viewportRef.current; if (!viewport || itemCount === 0) return; const frame = requestAnimationFrame(() => { if (viewport.scrollHeight <= viewport.clientHeight) return; if (typeof viewport.scrollTo === "function") { viewport.scrollTo({ top: viewport.scrollHeight, behavior: reduce ? "auto" : "smooth", }); } else { viewport.scrollTop = viewport.scrollHeight; } }); return () => cancelAnimationFrame(frame); }, [itemCount, reduce]); return (
{items.length ? (
    {items.map((item) => { const status = item.status ?? "pending"; return ( {statusLabel(status)}: {item.title} {item.detail ? ( {item.detail} ) : null} ); })}
) : (

No tasks yet

)}
); }