{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-list",
  "type": "registry:component",
  "title": "TaskList",
  "description": "Floating agent task progress pill above the composer that expands into the full task list.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/collapsible.json",
    "https://control-ui.dev/r/core.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/task-list-knobs.ts",
      "target": "@components/control-ui/knob-contracts/task-list-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/task-list.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const taskListKnobs = [\n  \"--cui-task-list-radius\",\n  \"--cui-task-list-background\",\n  \"--cui-task-list-foreground\",\n  \"--cui-task-list-border-color\",\n  \"--cui-task-list-shadow\",\n  \"--cui-task-list-item-foreground\",\n  \"--cui-task-list-item-pending-foreground\",\n  \"--cui-task-list-indicator-foreground\",\n  \"--cui-task-list-indicator-active-foreground\",\n] as const;\nexport type TaskListKnobStyle = Partial<Record<(typeof taskListKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/task-list.css",
      "target": "@components/control-ui/styles/recipes/task-list.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"task-list\"][data-slot=\"root\"]) {\n    --cui-task-list-background: oklch(from var(--card) l c h / 0.9);\n    --cui-task-list-border-color: var(--border);\n    --cui-task-list-foreground: var(--foreground);\n    --cui-task-list-radius: var(--radius-field);\n    --cui-task-list-shadow: var(--shadow-md);\n    --cui-task-list-indicator-active-foreground: var(--primary-text);\n    --cui-task-list-indicator-foreground: oklch(from var(--muted-foreground) l c h / 0.7);\n    --cui-task-list-item-foreground: var(--foreground);\n    --cui-task-list-item-pending-foreground: var(--muted-foreground);\n    border: 1px solid var(--cui-task-list-border-color);\n    border-radius: var(--cui-task-list-radius);\n    background: var(--cui-task-list-background);\n    color: var(--cui-task-list-foreground);\n    box-shadow:\n      inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.04),\n      var(--cui-task-list-shadow);\n    backdrop-filter: blur(var(--backdrop-blur-popover));\n    font-size: var(--text-body);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"trigger\"]) {\n    font-size: var(--text-sm);\n    transition: background-color var(--duration-fast) var(--ease-standard);\n    text-align: left;\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"task-list\"][data-slot=\"trigger\"]:hover) {\n      background: oklch(from var(--muted) l c h / 0.4);\n    }\n  }\n\n  :where([data-control-family=\"task-list\"]:is([data-slot=\"progress\"], [data-slot=\"chevron\"])) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"label\"]) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"item\"]) {\n    border-radius: var(--radius-popup-item);\n    color: var(--cui-task-list-item-foreground);\n    font-size: var(--text-sm);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"item\"][data-status=\"pending\"]) {\n    color: var(--cui-task-list-item-pending-foreground);\n    opacity: 0.7;\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"item-indicator\"]) {\n    color: var(--cui-task-list-indicator-foreground);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"item-indicator\"]:is([data-status=\"active\"], [data-status=\"completed\"])) {\n    color: var(--cui-task-list-indicator-active-foreground);\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"item-indicator\"][data-status=\"active\"] > svg) {\n    animation: task-list-spin calc(var(--duration-slow) * 2) linear infinite;\n  }\n\n  :where([data-control-family=\"task-list\"][data-slot=\"progress\"]) {\n    font-weight: var(--font-weight-medium);\n    font-variant-numeric: tabular-nums;\n  }\n}\n\n@keyframes task-list-spin {\n  to {\n    rotate: 360deg;\n  }\n}\n\n@property --cui-task-list-radius {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-task-list-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-border-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-task-list-item-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-item-pending-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-indicator-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-task-list-indicator-active-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/task-list.tsx",
      "target": "@components/control-ui/task-list.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ChevronRight, CircleCheck, CircleDashed, Loader2 } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, ReactNode } from \"react\";\nimport { createContext, useContext, useEffect, useId, useState } from \"react\";\n\nimport type { TaskListKnobStyle } from \"@/components/control-ui/knob-contracts/task-list-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport type { CollapsibleProps } from \"@/components/control-ui/ui/collapsible\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/control-ui/ui/collapsible\";\n\nexport type TaskStatus = \"pending\" | \"active\" | \"completed\";\n\nexport type TaskListProps = Omit<CollapsibleProps, \"style\"> & { style?: CSSProperties & TaskListKnobStyle };\n\ntype TaskListStyleProps<Props, Style> = Omit<Props, \"style\"> & { style?: CSSProperties & Style };\n\ntype TaskEntry = {\n  key: string;\n  status: TaskStatus;\n  label: string;\n};\n\ntype TaskListRegistrationContextValue = {\n  registerTask: (entry: TaskEntry) => void;\n  unregisterTask: (key: string) => void;\n};\n\ntype TaskListContextValue = {\n  total: number;\n  current?: TaskEntry;\n  /** 1-based position rendered as \"Task 3 of 5\"; 0 while no task is registered. */\n  currentNumber: number;\n  allCompleted: boolean;\n};\n\nconst TaskListContext = createContext<TaskListContextValue | null>(null);\n\nconst TaskListRegistrationContext = createContext<TaskListRegistrationContextValue | null>(null);\n\nfunction useTaskListContext() {\n  const context = useContext(TaskListContext);\n  if (!context) throw new Error(\"TaskList compound components must be rendered inside <TaskList>.\");\n  return context;\n}\n\nfunction useTaskListRegistrationContext() {\n  const context = useContext(TaskListRegistrationContext);\n  if (!context) throw new Error(\"TaskList items must be rendered inside <TaskList>.\");\n  return context;\n}\n\nfunction upsert(entries: TaskEntry[], entry: TaskEntry) {\n  const index = entries.findIndex((existing) => existing.key === entry.key);\n  if (index === -1) return [...entries, entry];\n  const next = [...entries];\n  next[index] = entry;\n  return next;\n}\n\nfunction currentTaskIndex(tasks: TaskEntry[]) {\n  const active = tasks.findIndex((task) => task.status === \"active\");\n  if (active !== -1) return active;\n  const pending = tasks.findIndex((task) => task.status === \"pending\");\n  if (pending !== -1) return pending;\n  return tasks.length - 1;\n}\n\nexport function TaskList({ className, children, ...props }: TaskListProps) {\n  const [tasks, setTasks] = useState<TaskEntry[]>([]);\n  // register upserts in place so status change never reorders, and unregister runs only at item unmount —\n  // combined effect cleanup would move every updated item to end\n  const [registration] = useState<TaskListRegistrationContextValue>(() => ({\n    registerTask(entry) {\n      setTasks((previous) => upsert(previous, entry));\n    },\n    unregisterTask(key) {\n      setTasks((previous) => previous.filter((task) => task.key !== key));\n    },\n  }));\n\n  const total = tasks.length;\n  const currentIndex = currentTaskIndex(tasks);\n  const current = tasks[currentIndex];\n  const allCompleted = total > 0 && tasks.every((task) => task.status === \"completed\");\n\n  return (\n    <TaskListRegistrationContext.Provider value={registration}>\n      <TaskListContext.Provider value={{ total, current, currentNumber: total === 0 ? 0 : currentIndex + 1, allCompleted }}>\n        <Collapsible\n          data-control-ui=\"task-list\"\n          data-control-family=\"task-list\"\n          data-slot=\"root\"\n          data-surface=\"panel\"\n          className={cn(\"w-full overflow-hidden\", className)}\n          {...props}\n        >\n          {children}\n        </Collapsible>\n      </TaskListContext.Provider>\n    </TaskListRegistrationContext.Provider>\n  );\n}\n\nexport type TaskListTriggerProps = TaskListStyleProps<ComponentProps<\"button\">, TaskListKnobStyle>;\n\nexport function TaskListTrigger({ className, children, ...props }: TaskListTriggerProps) {\n  const { allCompleted } = useTaskListContext();\n\n  return (\n    <CollapsibleTrigger\n      data-control-ui=\"task-list\"\n      data-control-family=\"task-list\"\n      data-slot=\"trigger\"\n      className={cn(\"flex w-full items-center gap-2 px-3 py-2\", className)}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <TaskListIndicator status={allCompleted ? \"completed\" : \"active\"} />\n          <TaskListProgress />\n          <ChevronRight\n            aria-hidden=\"true\"\n            data-control-ui=\"task-list\"\n            data-control-family=\"task-list\"\n            data-slot=\"chevron\"\n            className=\"size-3.5 shrink-0\"\n          />\n          {/* expanded list already shows current task, so pill's preview hides while open */}\n          <TaskListLabel className=\"in-data-[state=open]:hidden\" />\n        </>\n      )}\n    </CollapsibleTrigger>\n  );\n}\n\nexport type TaskListProgressProps = TaskListStyleProps<ComponentProps<\"span\">, TaskListKnobStyle>;\n\nexport function TaskListProgress({ className, children, ...props }: TaskListProgressProps) {\n  const { currentNumber, total } = useTaskListContext();\n\n  return (\n    <span data-control-ui=\"task-list\" data-control-family=\"task-list\" data-slot=\"progress\" className={cn(\"shrink-0\", className)} {...props}>\n      {children ?? `Task ${currentNumber} of ${total}`}\n    </span>\n  );\n}\n\nexport type TaskListLabelProps = TaskListStyleProps<ComponentProps<\"span\">, TaskListKnobStyle>;\n\nexport function TaskListLabel({ className, children, ...props }: TaskListLabelProps) {\n  const { current } = useTaskListContext();\n\n  return (\n    <span\n      data-control-ui=\"task-list\"\n      data-control-family=\"task-list\"\n      data-slot=\"label\"\n      className={cn(\"min-w-0 flex-1 truncate\", className)}\n      {...props}\n    >\n      {children ?? current?.label}\n    </span>\n  );\n}\n\nexport type TaskListContentProps = ComponentProps<\"ol\"> & { style?: CSSProperties & TaskListKnobStyle };\n\nexport function TaskListContent({ className, children, ...props }: TaskListContentProps) {\n  return (\n    // closed items must stay registered — collapsed pill derives \"Task 3 of 5\" from them\n    <CollapsibleContent keepMounted>\n      <ol\n        data-control-ui=\"task-list\"\n        data-control-family=\"task-list\"\n        data-slot=\"items\"\n        className={cn(\"flex flex-col gap-0.5 px-2 pb-2\", className)}\n        {...props}\n      >\n        {children}\n      </ol>\n    </CollapsibleContent>\n  );\n}\n\nexport type TaskListItemProps = TaskListStyleProps<Omit<ComponentProps<\"li\">, \"children\">, TaskListKnobStyle> & {\n  label: string;\n  status?: TaskStatus;\n  children?: ReactNode;\n};\n\nexport function TaskListItem({ label, status = \"pending\", className, children, ...props }: TaskListItemProps) {\n  const { registerTask, unregisterTask } = useTaskListRegistrationContext();\n  const key = useId();\n\n  useEffect(() => registerTask({ key, status, label }), [registerTask, key, status, label]);\n  useEffect(() => () => unregisterTask(key), [unregisterTask, key]);\n\n  return (\n    <li\n      data-control-ui=\"task-list\"\n      data-control-family=\"task-list\"\n      data-slot=\"item\"\n      data-status={status}\n      className={cn(\"flex items-center gap-2 px-1.5 py-1\", className)}\n      {...props}\n    >\n      <TaskListIndicator status={status} />\n      <span className=\"min-w-0 flex-1 truncate\">{children ?? label}</span>\n    </li>\n  );\n}\n\nconst indicatorIcons = {\n  pending: CircleDashed,\n  active: Loader2,\n  completed: CircleCheck,\n} as const;\n\nexport type TaskListIndicatorProps = TaskListStyleProps<ComponentProps<\"span\">, TaskListKnobStyle> & {\n  status?: TaskStatus;\n};\n\nexport function TaskListIndicator({ status = \"pending\", className, ...props }: TaskListIndicatorProps) {\n  const Icon = indicatorIcons[status];\n\n  return (\n    <span\n      data-control-ui=\"task-list\"\n      data-control-family=\"task-list\"\n      data-slot=\"item-indicator\"\n      data-status={status}\n      className={cn(\"inline-flex shrink-0 items-center justify-center\", className)}\n      {...props}\n    >\n      {/* loader, not expressive motion — like Spinner it keeps turning under reduced motion */}\n      <Icon aria-hidden=\"true\" className=\"size-3.5\" />\n      <span className=\"sr-only\">{status}</span>\n    </span>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/task-list.css\"": {}
  },
  "meta": {}
}
