{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"todo-list","type":"registry:component","title":"Todo List","description":"A collapsible agent task plan with morphing status marks and a completion count.","author":"Ryan","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/todo-list.tsx","type":"registry:component","target":"@components/motion/todo-list.tsx","content":"\"use client\";\n// easeui.dev/components/agents/todo-list\n\nimport { Check, ChevronDown, Loader2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TodoStatus = \"pending\" | \"active\" | \"done\";\n\nexport interface TodoItem {\n  id: string;\n  label: string;\n  status: TodoStatus;\n  /** Short detail under the label, such as a file name. */\n  meta?: string;\n}\n\nexport interface TodoListProps {\n  /** Heading shown next to the completion count. Default \"Tasks\". */\n  title?: string;\n  items: TodoItem[];\n  /** Starting expanded state. Default true. */\n  defaultOpen?: boolean;\n  className?: string;\n}\n\nconst EASE = [0.23, 1, 0.32, 1] as const;\n\nfunction StatusMark({ status }: { status: TodoStatus }) {\n  const reduce = useReducedMotion();\n  const transition = { duration: reduce ? 0 : 0.15, ease: EASE };\n\n  return (\n    <span className=\"relative flex h-4 w-4 shrink-0 items-center justify-center\">\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {status === \"done\" ? (\n          <motion.span\n            key=\"done\"\n            initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.6 }}\n            transition={transition}\n            className=\"flex h-4 w-4 items-center justify-center rounded-full bg-accent text-accent-foreground\"\n          >\n            <Check aria-hidden=\"true\" strokeWidth={3} className=\"h-2.5 w-2.5\" />\n          </motion.span>\n        ) : status === \"active\" ? (\n          <motion.span\n            key=\"active\"\n            initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.6 }}\n            transition={transition}\n          >\n            <Loader2 aria-hidden=\"true\" className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n          </motion.span>\n        ) : (\n          <motion.span\n            key=\"pending\"\n            initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.6 }}\n            transition={transition}\n            className=\"h-3 w-3 rounded-full shadow-[0_0_0_1.5px_var(--border-strong)]\"\n          />\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/**\n * A collapsible task plan. The header shows how many items are done, and\n * each status mark morphs as a task moves from pending to active to done.\n */\nexport function TodoList({ title = \"Tasks\", items, defaultOpen = true, className }: TodoListProps) {\n  const [open, setOpen] = useState(defaultOpen);\n  const panelId = useId();\n  const done = items.filter((item) => item.status === \"done\").length;\n\n  return (\n    <div className={cn(\"rounded-2xl bg-card shadow-[0_0_0_1px_var(--border)]\", className)}>\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        aria-controls={panelId}\n        onClick={() => setOpen((value) => !value)}\n        className=\"flex min-h-11 w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-foreground/40\"\n      >\n        <span className=\"flex items-center gap-2\">\n          {title}\n          <span className=\"text-xs font-normal tabular-nums text-muted-foreground\">\n            {done}/{items.length}\n          </span>\n        </span>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={cn(\n            \"h-4 w-4 text-muted-foreground transition-transform duration-200 ease-out motion-reduce:transition-none\",\n            open && \"rotate-180\",\n          )}\n        />\n      </button>\n      <div\n        id={panelId}\n        inert={!open}\n        className={cn(\n          \"grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none\",\n          open ? \"grid-rows-[1fr]\" : \"grid-rows-[0fr]\",\n        )}\n      >\n        <div className=\"overflow-hidden\">\n          <ul className=\"flex flex-col gap-2.5 px-4 pb-4\">\n            {items.map((item) => (\n              <li key={item.id} className=\"flex items-start gap-2.5\">\n                <span className=\"mt-0.5\">\n                  <StatusMark status={item.status} />\n                </span>\n                <span className=\"min-w-0 flex-1\">\n                  <p\n                    className={cn(\n                      \"text-sm leading-5\",\n                      item.status === \"done\"\n                        ? \"text-muted-foreground line-through\"\n                        : \"text-foreground\",\n                    )}\n                  >\n                    {item.label}\n                  </p>\n                  {item.meta ? (\n                    <p className=\"mt-0.5 text-xs text-muted-foreground\">{item.meta}</p>\n                  ) : null}\n                </span>\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}