{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "todo-tool",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://agent-elements.21st.dev/r/text-shimmer.json"
  ],
  "files": [
    {
      "path": "registry/agent-elements/components/tools/todo-tool.tsx",
      "content": "import { memo, useMemo } from \"react\";\nimport { CheckIcon, IconArrowRight } from \"../icons\";\nimport { TextShimmer } from \"../text-shimmer\";\nimport { getToolStatus, areToolPropsEqual } from \"../utils/format-tool\";\nimport { cn } from \"../utils/cn\";\n\nexport type TodoItem = {\n  content: string;\n  status: \"pending\" | \"in_progress\" | \"completed\";\n  activeForm?: string;\n};\n\nexport type TodoToolProps = {\n  part: any;\n  chatStatus?: string;\n};\n\nexport type TodoChange = {\n  todo: TodoItem;\n  oldStatus?: TodoItem[\"status\"];\n  newStatus: TodoItem[\"status\"];\n  index: number;\n};\n\ntype ChangeType = \"creation\" | \"single\" | \"multiple\";\n\nexport type DetectedChanges = {\n  type: ChangeType;\n  items: TodoChange[];\n};\n\nfunction detectChanges(\n  oldTodos: TodoItem[],\n  newTodos: TodoItem[],\n): DetectedChanges {\n  if (!oldTodos || oldTodos.length === 0) {\n    return {\n      type: \"creation\",\n      items: newTodos.map((todo, index) => ({\n        todo,\n        newStatus: todo.status,\n        index,\n      })),\n    };\n  }\n\n  const changes: TodoChange[] = [];\n  newTodos.forEach((newTodo, index) => {\n    const oldTodo = oldTodos[index];\n    if (!oldTodo || oldTodo.status !== newTodo.status) {\n      changes.push({\n        todo: newTodo,\n        oldStatus: oldTodo?.status,\n        newStatus: newTodo.status,\n        index,\n      });\n    }\n  });\n\n  if (changes.length === 1) return { type: \"single\", items: changes };\n  return { type: \"multiple\", items: changes };\n}\n\nconst TodoStatusIcon = ({\n  status,\n  isPending,\n}: {\n  status: TodoItem[\"status\"];\n  isPending?: boolean;\n}) => {\n  if (isPending && status === \"in_progress\") {\n    return (\n      <div className=\"w-3.5 h-3.5 rounded-full flex items-center justify-center shrink-0 border border-an-foreground-muted/60\">\n        <IconArrowRight className=\"w-2 h-2 text-an-foreground-muted/70\" />\n      </div>\n    );\n  }\n\n  switch (status) {\n    case \"completed\":\n      return (\n        <div className=\"w-3.5 h-3.5 rounded-full flex items-center justify-center shrink-0 border border-an-foreground-muted/40\">\n          <CheckIcon className=\"w-2 h-2 text-an-foreground-muted/70\" />\n        </div>\n      );\n    case \"in_progress\":\n      return (\n        <div className=\"w-3.5 h-3.5 rounded-full flex items-center justify-center shrink-0 border border-an-foreground-muted/60\">\n          <IconArrowRight className=\"w-2 h-2 text-an-foreground-muted/70\" />\n        </div>\n      );\n    default:\n      return (\n        <div className=\"w-3.5 h-3.5 rounded-full flex items-center justify-center shrink-0 border border-an-foreground-muted/60\" />\n      );\n  }\n};\n\nconst TodoListItem = memo(function TodoListItem({\n  todo,\n  isPending,\n}: {\n  todo: TodoItem;\n  isPending: boolean;\n}) {\n  return (\n    <div className={cn(\"flex items-start gap-2\")}>\n      <div className=\"mt-[2px]\">\n        <TodoStatusIcon status={todo.status} isPending={isPending} />\n      </div>\n      <span\n        className={cn(\n          \"text-sm\",\n          todo.status === \"completed\" && \"line-through\",\n          isPending || todo.status === \"completed\" || todo.status === \"pending\"\n            ? \"text-an-foreground/60\"\n            : \"text-an-foreground/80\",\n        )}\n      >\n        {todo.content}\n      </span>\n    </div>\n  );\n});\n\nexport const TodoTool = memo(function TodoTool({\n  part,\n  chatStatus,\n}: TodoToolProps) {\n  const { isPending } = getToolStatus(part, chatStatus);\n\n  const isStreaming = part.state === \"input-streaming\";\n  const oldTodos: TodoItem[] = part.output?.oldTodos || [];\n  const newTodos: TodoItem[] = part.input?.todos || part.output?.newTodos || [];\n\n  const isCreation = oldTodos.length === 0;\n  const changes = useMemo(\n    () => detectChanges(oldTodos, newTodos),\n    [oldTodos, newTodos],\n  );\n\n  // Streaming placeholder — always shimmer while in this transient state.\n  if (isStreaming || newTodos.length === 0) {\n    return (\n      <div className=\"space-y-2 text-sm leading-relaxed text-an-foreground/80\">\n        <div className=\"text-an-foreground/60\">\n          <TextShimmer\n            as=\"span\"\n            duration={1.2}\n            className=\"inline-flex items-center text-sm leading-none h-4 m-0\"\n          >\n            {isCreation ? \"Creating to-do list...\" : \"Updating to-dos...\"}\n          </TextShimmer>\n        </div>\n      </div>\n    );\n  }\n\n  // Single update - show full list for clarity\n  if (changes.type === \"single\") {\n    return (\n      <div className=\"space-y-2 text-sm leading-relaxed text-an-foreground/80\">\n        {newTodos.map((todo, idx) => (\n          <TodoListItem key={idx} todo={todo} isPending={isPending} />\n        ))}\n      </div>\n    );\n  }\n\n  // Multiple updates - show full list for clarity\n  if (changes.type === \"multiple\") {\n    return (\n      <div className=\"space-y-2 text-sm leading-relaxed text-an-foreground/80\">\n        {newTodos.map((todo, idx) => (\n          <TodoListItem key={idx} todo={todo} isPending={isPending} />\n        ))}\n      </div>\n    );\n  }\n\n  const displayTodos = newTodos;\n  return (\n    <div className=\"space-y-2 text-sm leading-relaxed text-an-foreground/80\">\n      {displayTodos.map((todo, idx) => (\n        <TodoListItem key={idx} todo={todo} isPending={isPending} />\n      ))}\n    </div>\n  );\n}, areToolPropsEqual);\n",
      "type": "registry:ui",
      "target": "components/agent-elements/tools/todo-tool.tsx"
    },
    {
      "path": "registry/agent-elements/icons.tsx",
      "content": "import React from \"react\";\n\ntype IconProps = React.SVGProps<SVGSVGElement> & {\n  className?: string;\n};\n\nexport function IconSpinner({ className, ...rest }: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"16\"\n      height=\"16\"\n      fill=\"none\"\n      className={\n        className ?? \"animate-spin will-change-transform text-muted-foreground\"\n      }\n      {...rest}\n    >\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"3\"\n        strokeLinecap=\"round\"\n        fill=\"none\"\n        opacity={0.2}\n      />\n      <path\n        d=\"M12 2C6.48 2 2 6.48 2 12\"\n        stroke=\"currentColor\"\n        strokeWidth=\"3\"\n        strokeLinecap=\"round\"\n        fill=\"none\"\n      />\n    </svg>\n  );\n}\n\nexport function CheckIcon(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      {...props}\n    >\n      <path\n        d=\"M5 12.75L10 19L19 5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function IconArrowRight(props: IconProps) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" width=\"24\" height=\"24\" {...props}>\n      <path\n        d=\"M14 6L20 12L14 18\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M19 12H4\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function IconDoubleChevronRight(props: IconProps) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" width=\"16\" height=\"16\" {...props}>\n      <path\n        d=\"M6 17L11 12L6 7M13 17L18 12L13 7\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/icons.tsx"
    },
    {
      "path": "registry/agent-elements/utils/format-tool.ts",
      "content": "/**\n * Tool state cache for detecting AI SDK in-place mutations.\n * AI SDK mutates objects in-place during streaming, so we must\n * cache state externally and compare cached values.\n */\n\ntype CachedToolState = {\n  state: string | undefined;\n  inputJson: string;\n  outputJson: string;\n};\n\nconst toolStateCache = new Map<string, CachedToolState>();\n\nfunction getToolStateSnapshot(part: any): CachedToolState {\n  return {\n    state: part.state,\n    inputJson: JSON.stringify(part.input || {}),\n    outputJson: JSON.stringify(part.output || {}),\n  };\n}\n\nfunction hasToolStateChanged(toolCallId: string, part: any): boolean {\n  const cached = toolStateCache.get(toolCallId);\n  const current = getToolStateSnapshot(part);\n\n  if (!cached) {\n    toolStateCache.set(toolCallId, current);\n    return true;\n  }\n\n  const changed =\n    cached.state !== current.state ||\n    cached.inputJson !== current.inputJson ||\n    cached.outputJson !== current.outputJson;\n\n  if (changed) {\n    toolStateCache.set(toolCallId, current);\n  }\n\n  return changed;\n}\n\nfunction arePartsEqual(prev: any, next: any): boolean {\n  if (prev.toolCallId !== next.toolCallId) return false;\n  if (prev.type !== next.type) return false;\n\n  const toolCallId = next.toolCallId;\n  if (!toolCallId) {\n    return prev.state === next.state;\n  }\n\n  const changed = hasToolStateChanged(toolCallId, next);\n  return !changed;\n}\n\nfunction isToolCompleted(part: any): boolean {\n  if (part.output !== undefined && part.output !== null) return true;\n  if (part.state === \"error\") return true;\n  if (part.state === \"result\") return true;\n  return false;\n}\n\n/** Deep compare function for tool part props. Used with React.memo(). */\nexport function areToolPropsEqual(\n  prevProps: { part: any; chatStatus?: string },\n  nextProps: { part: any; chatStatus?: string },\n): boolean {\n  const partsEqual = arePartsEqual(prevProps.part, nextProps.part);\n  if (!partsEqual) return false;\n  if (isToolCompleted(nextProps.part)) return true;\n  if (prevProps.chatStatus !== nextProps.chatStatus) return false;\n  return true;\n}\n\n/** Get tool status from part state */\nexport function getToolStatus(part: any, chatStatus?: string) {\n  const basePending =\n    part.state !== \"output-available\" && part.state !== \"output-error\";\n  const isError =\n    part.state === \"output-error\" ||\n    (part.state === \"output-available\" && part.output?.success === false);\n  const isSuccess = part.state === \"output-available\" && !isError;\n  const isPending = basePending && chatStatus === \"streaming\";\n  const isInterrupted =\n    basePending && chatStatus !== \"streaming\" && chatStatus !== undefined;\n\n  return { isPending, isError, isSuccess, isInterrupted };\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/format-tool.ts"
    },
    {
      "path": "registry/agent-elements/utils/cn.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",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/cn.ts"
    },
    {
      "path": "registry/agent-elements/agent-ui.css",
      "content": "/* Agent Elements tokens + utilities */\n\n/*\n * AN Agent Chat - CSS Custom Properties\n * These are the default values. Override by redefining these vars in your app CSS.\n */\n:root {\n  /* Geometry — all radii derive from --an-border-radius */\n  --an-border-radius: 16px;\n  --an-message-border-radius: var(--an-border-radius);\n  --an-input-border-radius: var(--an-border-radius);\n  --an-tool-border-radius: 10px;\n  --an-message-radius-inner-offset: 4px;\n  --an-max-width: 420px;\n\n  /* Colors - Light mode defaults */\n  --an-background: #ffffff;\n  --an-background-secondary: #f0f0f0;\n  --an-background-tertiary: #f8f8f8;\n  --an-foreground: #1a1a1a;\n  --an-foreground-muted: #737373;\n  --an-foreground-subtle: #a3a3a3;\n  --an-border-color: #e4e4e7;\n  --an-primary-color: #3b82f6;\n\n  /* User Messages */\n  --an-user-message-bg: #f5f5f5;\n  --an-user-message-text: #1a1a1a;\n\n  /* Input */\n  --an-input-background: #ffffff;\n  --an-input-border-color: #e4e4e7;\n  --an-input-color: #1a1a1a;\n  --an-input-placeholder-color: #a3a3a3;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 10px;\n\n  /* Send/Stop Buttons */\n  --an-send-button-bg: #3b82f6;\n  --an-send-button-color: #ffffff;\n\n  /* Tools */\n  --an-tool-background: #f5f5f5;\n  --an-tool-border-color: #e4e4e7;\n  --an-tool-color: #1a1a1a;\n  --an-tool-color-muted: #737373;\n\n  /* Code */\n  --an-code-background: #1e1e1e;\n  --an-code-color: #d4d4d4;\n\n  /* Diff colors */\n  --an-diff-added-bg: rgba(34, 197, 94, 0.1);\n  --an-diff-added-border: rgba(34, 197, 94, 0.5);\n  --an-diff-added-text: #15803d;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.1);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.5);\n  --an-diff-removed-text: #dc2626;\n}\n\n/* Dark mode defaults */\n.dark {\n  --an-background: #0a0a0a;\n  --an-background-secondary: #242424;\n  --an-background-tertiary: #141414;\n  --an-foreground: #fafafa;\n  --an-foreground-muted: #8c8c8c;\n  --an-foreground-subtle: #71717a;\n  --an-border-color: #2a2a2a;\n  --an-primary-color: #60a5fa;\n\n  --an-user-message-bg: #1a1a1a;\n  --an-user-message-text: #fafafa;\n\n  --an-input-background: #0a0a0a;\n  --an-input-border-color: #2a2a2a;\n  --an-input-color: #fafafa;\n  --an-input-placeholder-color: #71717a;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 12px;\n\n  --an-send-button-bg: #60a5fa;\n  /* Dark mode uses a lighter primary (#60a5fa) — pair it with black text so\n     labels like \"Approve\" / send-arrow have proper contrast instead of\n     low-contrast white-on-light-blue. */\n  --an-send-button-color: #0a0a0a;\n\n  --an-tool-background: #141414;\n  --an-tool-border-color: #2a2a2a;\n  --an-tool-color: #fafafa;\n  --an-tool-color-muted: #8c8c8c;\n\n  --an-code-background: #0a0a0a;\n  --an-code-color: #d4d4d4;\n\n  --an-diff-added-bg: rgba(34, 197, 94, 0.15);\n  --an-diff-added-border: rgba(34, 197, 94, 0.4);\n  --an-diff-added-text: #4ade80;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.15);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.4);\n  --an-diff-removed-text: #f87171;\n}\n\n@theme inline {\n  --color-an-background: var(--an-background);\n  --color-an-background-secondary: var(--an-background-secondary);\n  --color-an-background-tertiary: var(--an-background-tertiary);\n  --color-an-foreground: var(--an-foreground);\n  --color-an-foreground-muted: var(--an-foreground-muted);\n  --color-an-foreground-subtle: var(--an-foreground-subtle);\n  --color-an-border-color: var(--an-border-color);\n  --color-an-primary-color: var(--an-primary-color);\n  --color-an-user-message-bg: var(--an-user-message-bg);\n  --color-an-user-message-text: var(--an-user-message-text);\n  --color-an-input-background: var(--an-input-background);\n  --color-an-input-border-color: var(--an-input-border-color);\n  --color-an-input-color: var(--an-input-color);\n  --color-an-input-placeholder-color: var(--an-input-placeholder-color);\n  --color-an-input-focus-outline: var(--an-input-focus-outline);\n  --color-an-send-button-bg: var(--an-send-button-bg);\n  --color-an-send-button-color: var(--an-send-button-color);\n  --color-an-tool-background: var(--an-tool-background);\n  --color-an-tool-border-color: var(--an-tool-border-color);\n  --color-an-tool-color: var(--an-tool-color);\n  --color-an-tool-color-muted: var(--an-tool-color-muted);\n  --color-an-code-background: var(--an-code-background);\n  --color-an-code-color: var(--an-code-color);\n  --color-an-diff-added-bg: var(--an-diff-added-bg);\n  --color-an-diff-added-border: var(--an-diff-added-border);\n  --color-an-diff-added-text: var(--an-diff-added-text);\n  --color-an-diff-removed-bg: var(--an-diff-removed-bg);\n  --color-an-diff-removed-border: var(--an-diff-removed-border);\n  --color-an-diff-removed-text: var(--an-diff-removed-text);\n  --radius-an-message: var(--an-message-border-radius);\n  --radius-an-message-inner: calc(\n    var(--an-message-border-radius) - var(--an-message-radius-inner-offset)\n  );\n  --radius-an-input-border-radius: var(--an-input-border-radius);\n  --radius-an-tool-border-radius: var(--an-tool-border-radius);\n  --spacing-an-context-padding: var(--an-context-padding);\n  --max-width-an: var(--an-max-width);\n  --spacing-an-user-message-x: 14px;\n  --spacing-an-user-message-y: 10px;\n}\n\n/* TextShimmer animation */\n@keyframes an-shimmer {\n  from {\n    background-position: 100% center;\n  }\n  to {\n    background-position: 0% center;\n  }\n}\n\n@keyframes an-blink {\n  50% {\n    opacity: 0;\n  }\n}\n\n@keyframes loading-dots {\n  0%,\n  100% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes an-ellipsis {\n  0% {\n    width: 0;\n  }\n  33% {\n    width: 0.33em;\n  }\n  66% {\n    width: 0.66em;\n  }\n  100% {\n    width: 1em;\n  }\n}\n\n.an-text-shimmer {\n  display: inline-block;\n  background-size: 250% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  color: transparent;\n  background-image: linear-gradient(\n    90deg,\n    var(--an-foreground-subtle, #a3a3a3) 0%,\n    var(--an-foreground-subtle, #a3a3a3) 40%,\n    var(--an-foreground-muted, #737373) 50%,\n    var(--an-foreground-subtle, #a3a3a3) 60%,\n    var(--an-foreground-subtle, #a3a3a3) 100%\n  );\n  background-repeat: no-repeat;\n}\n\n.an-text-shimmer--active {\n  animation: an-shimmer var(--an-shimmer-duration, 2s) linear infinite;\n}\n\n.an-ellipsis {\n  display: inline-block;\n  overflow: hidden;\n  width: 0;\n  vertical-align: bottom;\n  animation: an-ellipsis 1.2s steps(1, end) infinite;\n}\n\n.an-markdown pre code {\n  counter-reset: none !important;\n}\n\n.an-markdown pre code > span::before {\n  content: none !important;\n  display: none !important;\n}\n\n.an-markdown pre code > span {\n  padding-left: 0 !important;\n}\n\n/* Diff */\n.an-edit-diff,\n.an-edit-diff pre,\n.an-edit-diff code {\n  font-size: 12px;\n}\n\n.dark .an-edit-tool-card {\n  background-color: #000;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card {\n  background-color: #000;\n}\n\n.dark .an-edit-tool-card .an-edit-diff,\n.dark .an-edit-tool-card .an-edit-diff pre,\n.dark .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff pre,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n/* Markdown Code Block */\n.an-markdown [data-streamdown=\"code-block\"] {\n  padding: 0;\n  border: 1px solid var(--an-border-color);\n  border-radius: var(--an-tool-border-radius);\n  background: transparent;\n  gap: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-header\"] {\n  padding: 0px 10px 0px 8px;\n  height: auto;\n  font-size: 12px;\n  height: 28px;\n  background: transparent;\n  border-bottom: 1px solid var(--color-an-tool-border-color);\n  background-color: var(--an-tool-background);\n  position: relative;\n}\n\n.an-markdown div:has(> [data-streamdown=\"code-block-actions\"]) {\n  position: absolute;\n  height: auto;\n  margin: 0;\n  top: 0;\n  right: 0;\n  height: 28px;\n  padding-right: 8px;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] {\n  background: transparent;\n  border: none;\n  backdrop-filter: none;\n  padding: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] button > svg {\n  width: 14px;\n  height: 14px;\n}\n\n.an-markdown [data-streamdown=\"code-block-body\"] {\n  margin: 0;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  background: transparent;\n  border: transparent;\n  overflow-x: auto;\n}\n\n.an-markdown [data-streamdown=\"code-block\"] pre,\n.an-markdown [data-streamdown=\"code-block\"] code {\n  font-size: 12px;\n  background: transparent;\n}\n",
      "type": "registry:style",
      "target": "components/agent-elements/agent-ui.css"
    }
  ],
  "css": {
    "@import \"../components/agent-elements/agent-ui.css\"": ""
  }
}