{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-list",
  "type": "registry:ui",
  "title": "Task List",
  "description": "The agent's plan as a todo list: segmented progress, shimmering current task, live and final durations, failure details, sub-tasks and a folded Completed group.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/kura-format.json",
    "https://kuro.abstergo.fyi/r/shimmer-text.json",
    "https://kuro.abstergo.fyi/r/status-glyph.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "https://kuro.abstergo.fyi/r/use-elapsed.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/task-list/index.ts",
      "type": "registry:ui",
      "target": "components/kura/task-list/index.ts",
      "content": "export { TaskList, type TaskListItem, type TaskListProps } from \"./task-list\";\n"
    },
    {
      "path": "registry/kura/task-list/task-list.tsx",
      "type": "registry:ui",
      "target": "components/kura/task-list/task-list.tsx",
      "content": "\"use client\";\n\nimport { ChevronRight, ListTodo } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { useElapsed } from \"@/hooks/kura/use-elapsed\";\nimport { cn } from \"@/lib/utils\";\nimport { formatClock, formatDuration } from \"@/lib/kura/format\";\nimport { ShimmerText } from \"@/components/kura/shimmer-text\";\nimport { type AgentStatus, StatusGlyph } from \"@/components/kura/status-glyph\";\n\nexport interface TaskListItem {\n  /** Stable key. Keep it stable across updates so rows don't remount. */\n  id: string;\n  /** Imperative, one line: \"Patch src/auth/client.ts\". */\n  title: React.ReactNode;\n  status: AgentStatus;\n  /** Secondary line. Always shown for failed and waiting items; for the others when given. */\n  detail?: React.ReactNode;\n  /** Final duration (ms) of a finished item, shown on the right in mono. */\n  durationMs?: number;\n  /** When a running item started (wall clock). Drives the live timer on the right. */\n  startedAt?: Date | number;\n  /** Sub-tasks, rendered indented under the item. One level is the sweet spot. */\n  children?: readonly TaskListItem[];\n}\n\nexport interface TaskListProps extends Omit<React.ComponentProps<\"div\">, \"title\" | \"onSelect\"> {\n  /** The plan, in order. */\n  items: readonly TaskListItem[];\n  /** Heading in the panel header. Default \"Plan\". */\n  title?: React.ReactNode;\n  /** Extra controls on the right of the header (e.g. an IconButton). */\n  actions?: React.ReactNode;\n  /** `panel` = card with header and segmented progress (default); `plain` = rows only, for sidebars. */\n  variant?: \"panel\" | \"plain\";\n  /** Fold the leading run of finished items into a \"Completed (n)\" row while work remains. Default true. */\n  collapseCompleted?: boolean;\n  /** Whether the \"Completed\" group is expanded (controlled). */\n  completedOpen?: boolean;\n  /** Initial state of the \"Completed\" group when uncontrolled. Default false. */\n  defaultCompletedOpen?: boolean;\n  onCompletedOpenChange?: (open: boolean) => void;\n  /** Highlighted item (e.g. the one whose output is shown next to the list). */\n  selectedId?: string | null;\n  /** Makes rows interactive (buttons with arrow-key navigation) and reports the clicked item. */\n  onSelect?: (id: string, item: TaskListItem) => void;\n  /** Rendered when `items` is empty. Default: a quiet \"No plan yet\" state. */\n  empty?: React.ReactNode;\n  /** Accessible name of the list. Default: \"Plan\". */\n  label?: string;\n}\n\nconst FINISHED = new Set<AgentStatus>([\"done\", \"skipped\"]);\n\nconst SPOKEN: Record<AgentStatus, string> = {\n  queued: \"pending\",\n  running: \"in progress\",\n  waiting: \"waiting for you\",\n  done: \"done\",\n  failed: \"failed\",\n  skipped: \"skipped\",\n  paused: \"paused\",\n};\n\nconst SEGMENT: Record<AgentStatus, string> = {\n  queued: \"bg-kura-hover\",\n  running: \"bg-kura-accent\",\n  waiting: \"bg-kura-warning\",\n  done: \"bg-kura-fg-muted\",\n  failed: \"bg-kura-danger\",\n  skipped: \"bg-kura-border-strong\",\n  paused: \"bg-kura-border-strong\",\n};\n\nconst rowEnter =\n  \"transition-[opacity,translate,background-color] duration-(--kura-duration-base) ease-kura-out starting:translate-y-[2px] starting:opacity-0 motion-reduce:transition-[opacity,background-color] motion-reduce:starting:translate-y-0\";\n\nfunction textColor(status: AgentStatus): string {\n  switch (status) {\n    case \"done\":\n    case \"skipped\":\n      return \"text-kura-fg-subtle\";\n    case \"queued\":\n    case \"paused\":\n      return \"text-kura-fg-muted\";\n    default:\n      return \"text-kura-fg\";\n  }\n}\n\nfunction Timer({ item }: { item: TaskListItem }) {\n  const running = item.status === \"running\";\n  const live = useElapsed(item.startedAt, running, 1000);\n  let text: string | null = null;\n  if (running && live != null) text = formatClock(live);\n  else if (item.durationMs != null) text = formatDuration(item.durationMs);\n  return (\n    <span\n      data-slot=\"task-list-duration\"\n      className={cn(\n        \"min-w-[4.5ch] shrink-0 text-right font-kura-mono text-[12px] tabular-nums\",\n        running ? \"text-kura-fg-muted\" : \"text-kura-fg-subtle\",\n      )}\n    >\n      {text}\n    </span>\n  );\n}\n\ninterface RowProps {\n  item: TaskListItem;\n  depth: number;\n  selectedId: string | null | undefined;\n  onSelect: TaskListProps[\"onSelect\"];\n}\n\nfunction Row({ item, depth, selectedId, onSelect }: RowProps) {\n  const { status } = item;\n  const interactive = onSelect != null;\n  const selected = selectedId === item.id;\n  const childDone = item.children?.filter((c) => FINISHED.has(c.status)).length ?? 0;\n  const showDetail =\n    item.detail != null &&\n    (status === \"failed\" || status === \"waiting\" || status === \"running\" || depth === 0);\n  const titleNode =\n    status === \"running\" ? (\n      <ShimmerText duration={2.4}>{item.title}</ShimmerText>\n    ) : (\n      <span>{item.title}</span>\n    );\n\n  const inner = (\n    <>\n      <span className=\"flex h-5 shrink-0 items-center\">\n        <StatusGlyph status={status} size=\"sm\" label={null} />\n      </span>\n      <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n        <span className=\"flex min-h-5 items-center gap-2\">\n          <span\n            data-slot=\"task-list-title\"\n            className={cn(\n              \"min-w-0 truncate text-[13px] leading-5 transition-colors duration-(--kura-duration-base)\",\n              textColor(status),\n              status === \"running\" && \"font-medium\",\n              status === \"skipped\" && \"line-through decoration-kura-border-strong\",\n              depth > 0 && \"text-[12.5px]\",\n            )}\n          >\n            {titleNode}\n            <span className=\"sr-only\"> ({SPOKEN[status]})</span>\n          </span>\n          {item.children && item.children.length > 0 && (\n            <span className=\"shrink-0 font-kura-mono text-[11.5px] text-kura-fg-subtle tabular-nums\">\n              {childDone}/{item.children.length}\n            </span>\n          )}\n        </span>\n        {showDetail && (\n          <span\n            data-slot=\"task-list-detail\"\n            className={cn(\n              \"text-[12px] leading-[18px]\",\n              status === \"failed\"\n                ? \"text-kura-danger-text\"\n                : status === \"waiting\"\n                  ? \"text-kura-warning-text\"\n                  : \"text-kura-fg-subtle\",\n            )}\n          >\n            {item.detail}\n          </span>\n        )}\n      </span>\n      <span className=\"flex h-5 items-center\">\n        <Timer item={item} />\n      </span>\n    </>\n  );\n\n  const rowClass = cn(\n    \"relative flex w-full items-start gap-2.5 py-1.5 pr-3.5 text-left\",\n    depth === 0 ? \"pl-3.5\" : \"pl-[calc(0.875rem+24px)]\",\n    selected &&\n      \"bg-kura-active before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:bg-kura-accent\",\n  );\n\n  return (\n    <li\n      data-slot=\"task-list-item\"\n      data-status={status}\n      data-selected={selected || undefined}\n      aria-current={status === \"running\" ? \"step\" : undefined}\n      className={cn(rowEnter, depth === 0 && \"border-kura-border-subtle border-t first:border-t-0\")}\n    >\n      {interactive ? (\n        <button\n          type=\"button\"\n          data-slot=\"task-list-row\"\n          data-task-row=\"\"\n          aria-pressed={selected}\n          onClick={() => onSelect(item.id, item)}\n          className={cn(\n            rowClass,\n            \"cursor-default outline-none transition-colors duration-(--kura-duration-instant) hover:bg-kura-hover focus-visible:shadow-[inset_0_0_0_2px_var(--kura-ring)]\",\n            selected && \"hover:bg-kura-active\",\n          )}\n        >\n          {inner}\n        </button>\n      ) : (\n        <div data-slot=\"task-list-row\" className={rowClass}>\n          {inner}\n        </div>\n      )}\n      {item.children && item.children.length > 0 && (\n        <ul data-slot=\"task-list-children\" className=\"relative pb-1\">\n          <span\n            aria-hidden\n            className=\"absolute top-0 bottom-2.5 left-[calc(0.875rem+6.5px)] w-px bg-kura-border-subtle\"\n          />\n          {item.children.map((child) => (\n            <Row\n              key={child.id}\n              item={child}\n              depth={depth + 1}\n              selectedId={selectedId}\n              onSelect={onSelect}\n            />\n          ))}\n        </ul>\n      )}\n    </li>\n  );\n}\n\nfunction sumDurations(items: readonly TaskListItem[]): number {\n  return items.reduce((acc, i) => acc + (i.durationMs ?? 0), 0);\n}\n\n/**\n * The agent's plan as a todo list (Claude Code TodoWrite style): a header with \"3 of 7\" and a\n * segmented progress bar, one row per task with a status glyph, a shimmering title for the running\n * task, live and final durations in mono, details for failures, nested sub-tasks, and the finished\n * head of the plan folded into a \"Completed (n)\" row.\n */\nexport function TaskList({\n  items,\n  title = \"Plan\",\n  actions,\n  variant = \"panel\",\n  collapseCompleted = true,\n  completedOpen: completedOpenProp,\n  defaultCompletedOpen = false,\n  onCompletedOpenChange,\n  selectedId,\n  onSelect,\n  empty,\n  label,\n  className,\n  onKeyDown,\n  ...props\n}: TaskListProps) {\n  const [completedOpen, setCompletedOpen] = useControllableState({\n    value: completedOpenProp,\n    defaultValue: defaultCompletedOpen,\n    onChange: onCompletedOpenChange,\n  });\n  const groupId = React.useId();\n\n  const total = items.length;\n  const doneCount = items.filter((i) => FINISHED.has(i.status)).length;\n  const runningIndex = items.findIndex((i) => i.status === \"running\");\n  const failedItem = items.find((i) => i.status === \"failed\");\n  const waitingItem = items.find((i) => i.status === \"waiting\");\n  const allFinished = total > 0 && doneCount === total;\n\n  // The leading run of finished items folds away while there is still work ahead.\n  let lead = 0;\n  while (lead < items.length && FINISHED.has(items[lead]!.status)) lead++;\n  const fold = collapseCompleted && !allFinished && lead >= 3;\n  const head = fold ? items.slice(0, lead) : [];\n  const rest = fold ? items.slice(lead) : items;\n\n  const runningTitle = runningIndex >= 0 ? items[runningIndex]!.title : null;\n  const announcement = failedItem\n    ? `Task failed: ${typeof failedItem.title === \"string\" ? failedItem.title : \"\"}`\n    : waitingItem\n      ? \"Waiting for you\"\n      : allFinished\n        ? \"All tasks complete\"\n        : runningIndex >= 0\n          ? `Task ${runningIndex + 1} of ${total}${typeof runningTitle === \"string\" ? `: ${runningTitle}` : \"\"}`\n          : \"\";\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented || !onSelect) return;\n    if (![\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(e.key)) return;\n    const rows = Array.from(\n      e.currentTarget.querySelectorAll<HTMLElement>(\"[data-task-row], [data-task-group]\"),\n    );\n    const index = rows.indexOf(document.activeElement as HTMLElement);\n    if (index < 0) return;\n    e.preventDefault();\n    const next =\n      e.key === \"Home\"\n        ? 0\n        : e.key === \"End\"\n          ? rows.length - 1\n          : Math.min(rows.length - 1, Math.max(0, index + (e.key === \"ArrowDown\" ? 1 : -1)));\n    rows[next]?.focus();\n  };\n\n  const listLabel = label ?? (typeof title === \"string\" ? title : \"Plan\");\n\n  const body =\n    total === 0 ? (\n      <div\n        data-slot=\"task-list-empty\"\n        className=\"flex flex-col items-center gap-1 px-6 py-8 text-center\"\n      >\n        {empty ?? (\n          <>\n            <ListTodo aria-hidden strokeWidth={1.75} className=\"mb-1 size-4 text-kura-fg-subtle\" />\n            <span className=\"font-medium text-[13px] text-kura-fg\">No plan yet</span>\n            <span className=\"text-[12px] text-kura-fg-subtle\">\n              Tasks appear here once the agent writes its plan.\n            </span>\n          </>\n        )}\n      </div>\n    ) : (\n      <ul aria-label={listLabel} data-slot=\"task-list-list\" className=\"flex flex-col py-1\">\n        {fold && (\n          <li data-slot=\"task-list-completed\" data-state={completedOpen ? \"open\" : \"closed\"}>\n            <button\n              type=\"button\"\n              data-task-group=\"\"\n              aria-expanded={completedOpen}\n              aria-controls={groupId}\n              onClick={() => setCompletedOpen((o) => !o)}\n              className=\"flex h-8 w-full cursor-default items-center gap-2.5 px-3.5 text-left text-[13px] text-kura-fg-subtle outline-none transition-colors duration-(--kura-duration-instant) hover:bg-kura-hover hover:text-kura-fg-muted focus-visible:shadow-[inset_0_0_0_2px_var(--kura-ring)]\"\n            >\n              <span className=\"grid size-3.5 place-items-center\">\n                <ChevronRight\n                  aria-hidden\n                  strokeWidth={2}\n                  className={cn(\n                    \"size-3.5 transition-transform duration-(--kura-duration-fast) ease-kura-out motion-reduce:transition-none\",\n                    completedOpen && \"rotate-90\",\n                  )}\n                />\n              </span>\n              <span className=\"flex-1\">\n                Completed <span className=\"font-kura-mono tabular-nums\">({head.length})</span>\n              </span>\n              <span className=\"font-kura-mono text-[12px] tabular-nums\">\n                {formatDuration(sumDurations(head))}\n              </span>\n            </button>\n            <ul id={groupId} hidden={!completedOpen} aria-label=\"Completed tasks\" className=\"pb-1\">\n              {head.map((item) => (\n                <Row\n                  key={item.id}\n                  item={item}\n                  depth={0}\n                  selectedId={selectedId}\n                  onSelect={onSelect}\n                />\n              ))}\n            </ul>\n          </li>\n        )}\n        {rest.map((item) => (\n          <Row key={item.id} item={item} depth={0} selectedId={selectedId} onSelect={onSelect} />\n        ))}\n      </ul>\n    );\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: arrow-key roving between the native row buttons inside.\n    <div\n      data-slot=\"task-list\"\n      data-variant={variant}\n      data-progress={`${doneCount}/${total}`}\n      className={cn(\n        \"flex min-w-0 flex-col font-kura-sans text-kura-fg\",\n        variant === \"panel\" && \"overflow-hidden rounded-kura-xl bg-kura-surface shadow-kura-sm\",\n        className,\n      )}\n      onKeyDown={handleKeyDown}\n      {...props}\n    >\n      {variant === \"panel\" && (\n        <div\n          data-slot=\"task-list-header\"\n          className=\"flex h-10 shrink-0 items-center gap-3 border-kura-border-subtle border-b px-3.5\"\n        >\n          <span className=\"font-medium text-[13px] tracking-[-0.005em]\">{title}</span>\n          {total > 0 && (\n            <span className=\"font-kura-mono text-[12px] text-kura-fg-subtle tabular-nums\">\n              {doneCount} of {total}\n            </span>\n          )}\n          <span className=\"flex-1\" />\n          {total > 0 && (\n            <span\n              data-slot=\"task-list-progress\"\n              aria-hidden\n              className=\"flex w-[clamp(64px,28%,140px)] items-center gap-[3px]\"\n            >\n              {items.map((item) => (\n                <span\n                  key={item.id}\n                  data-status={item.status}\n                  className={cn(\n                    \"h-1 min-w-0 flex-1 rounded-full transition-colors duration-(--kura-duration-slow) ease-kura-out\",\n                    SEGMENT[item.status],\n                    item.status === \"running\" && \"animate-kura-pulse motion-reduce:animate-none\",\n                  )}\n                />\n              ))}\n            </span>\n          )}\n          {actions}\n        </div>\n      )}\n      {body}\n      <span role=\"status\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "agent",
    "agent",
    "plan",
    "todo",
    "tasks"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/task-list · Markdown for agents: https://kuro.abstergo.fyi/components/task-list.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Show the todo list a coding agent maintains while it works (Claude Code TodoWrite, Codex plan, Cursor tasks) in a run sidebar or panel.",
        "Give a dense, scannable answer to 'what is the agent doing and what is left?' — '3 of 7' plus one row per task.",
        "Let the user click a task to show its output or diff next to the list (`onSelect` + `selectedId`).",
        "Show a failed task with its reason (`detail`) and a task blocked on the human (`waiting`)."
      ],
      "whenNotToUse": [
        "A narrative step timeline with connectors and inline tool calls under each step — use agent-timeline.",
        "Parallel subagents working at the same time — use agent-fleet.",
        "Timing analysis of nested spans on a shared time axis — use trace-waterfall.",
        "A single tool invocation with arguments and result — use tool-call."
      ]
    }
  }
}
