{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan-checklist",
  "title": "Plan Checklist",
  "description": "The agent's stated intentions before and while it acts — pending, in-progress, and done steps, live-editable and reorderable while they're still ahead.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/trovecn/ai-workbench/plan-checklist.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n  CircleIcon,\n  Loader2Icon,\n  PencilIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { spring } from \"@/lib/springs\";\nimport { cn } from \"@/lib/utils\";\n\nexport type PlanChecklistItemStatus = \"pending\" | \"in-progress\" | \"done\";\n\nexport interface PlanChecklistItem {\n  id: string;\n  /** The stated step, in the agent's own words — editable text, not markup. */\n  label: string;\n  status: PlanChecklistItemStatus;\n  /** A short supporting note under the label — scope, a file count. */\n  detail?: ReactNode;\n}\n\nexport interface PlanChecklistProps {\n  /** Caller-owned source of truth, in the order the plan will run. */\n  items: readonly PlanChecklistItem[];\n  title?: ReactNode;\n  /** Enables inline rename on not-yet-started items. Omit to render fixed text. */\n  onRelabel?: (id: string, label: string) => void;\n  /**\n   * Enables reordering not-yet-started items via Move up/down. Omit to\n   * render a fixed order. Called with the full, reordered id list.\n   */\n  onReorder?: (orderedIds: string[]) => void;\n  className?: string;\n}\n\nconst STATUS_LABEL: Record<PlanChecklistItemStatus, string> = {\n  pending: \"Pending\",\n  \"in-progress\": \"In progress\",\n  done: \"Done\",\n};\n\nfunction PlanChecklistStatusIcon({\n  status,\n  reduceMotion,\n}: {\n  status: PlanChecklistItemStatus;\n  reduceMotion: boolean;\n}) {\n  switch (status) {\n    case \"pending\":\n      return <CircleIcon className=\"size-3.5 text-muted-foreground\" />;\n    case \"in-progress\":\n      return (\n        <Loader2Icon\n          className={cn(\"size-3.5 text-muted-foreground\", !reduceMotion && \"animate-spin\")}\n        />\n      );\n    case \"done\":\n      return <CheckIcon className=\"size-3.5 text-success\" />;\n  }\n}\n\nfunction PlanChecklistStatus({\n  status,\n  reduceMotion,\n}: {\n  status: PlanChecklistItemStatus;\n  reduceMotion: boolean;\n}) {\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false}>\n      <motion.span\n        key={status}\n        initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.7 }}\n        animate={{ opacity: 1, scale: 1 }}\n        exit={{ opacity: 0, transition: spring.quick.exit }}\n        transition={reduceMotion ? spring.quick.exit : spring.moderate.enter}\n        className=\"flex size-4 shrink-0 items-center justify-center\"\n        aria-hidden=\"true\"\n      >\n        <PlanChecklistStatusIcon status={status} reduceMotion={reduceMotion} />\n      </motion.span>\n    </AnimatePresence>\n  );\n}\n\nfunction PlanChecklistLabel({\n  item,\n  editable,\n  reduceMotion,\n  onRelabel,\n}: {\n  item: PlanChecklistItem;\n  editable: boolean;\n  reduceMotion: boolean;\n  onRelabel?: (id: string, label: string) => void;\n}) {\n  const [isEditing, setIsEditing] = useState(false);\n  const [draft, setDraft] = useState(item.label);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (isEditing) inputRef.current?.focus();\n  }, [isEditing]);\n\n  function commit() {\n    const next = draft.trim();\n    setIsEditing(false);\n    if (next && next !== item.label) onRelabel?.(item.id, next);\n    else setDraft(item.label);\n  }\n\n  function cancel() {\n    setDraft(item.label);\n    setIsEditing(false);\n  }\n\n  function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n    if (event.key === \"Enter\") {\n      event.preventDefault();\n      inputRef.current?.blur();\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      cancel();\n    }\n  }\n\n  if (isEditing) {\n    return (\n      <input\n        ref={inputRef}\n        value={draft}\n        onChange={(event) => setDraft(event.target.value)}\n        onKeyDown={handleKeyDown}\n        onBlur={commit}\n        onFocus={(event) => event.currentTarget.select()}\n        aria-label=\"Step label\"\n        className=\"w-full min-w-0 rounded-sm bg-transparent text-control leading-snug text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      />\n    );\n  }\n\n  if (!editable) {\n    return (\n      <span\n        className={cn(\n          \"min-w-0 text-control leading-snug text-foreground transition-colors duration-quick\",\n          item.status === \"done\" && \"text-muted-foreground line-through\",\n          item.status === \"in-progress\" && !reduceMotion && \"agent-activity-shimmer\",\n        )}\n      >\n        {item.label}\n      </span>\n    );\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setIsEditing(true)}\n      className=\"group/label -mx-1 inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-sm px-1 text-left text-control leading-snug text-foreground transition-colors duration-quick hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n    >\n      <span className=\"truncate\">{item.label}</span>\n      <PencilIcon className=\"size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity duration-quick group-hover/label:opacity-100 group-focus-visible/label:opacity-100\" />\n    </button>\n  );\n}\n\n/**\n * The agent's stated intentions before and while it acts — distinct from\n * AgentActivity, which narrates what already happened. Only steps that\n * haven't started can be renamed or reordered: once a step is in progress or\n * done it becomes a record of what happened, not a draft, so relabel/reorder\n * affordances disappear the moment a step leaves \"pending\". Reordering ships\n * as Move up/down rather than drag so keyboard and screen-reader users get\n * the same capability as a pointer, from the first version, not a bolted-on\n * fallback.\n */\nfunction PlanChecklist({\n  items,\n  title = \"Plan\",\n  onRelabel,\n  onReorder,\n  className,\n}: PlanChecklistProps) {\n  const reduceMotion = useReducedMotion();\n  const statusId = useId();\n  const titleId = useId();\n\n  const doneCount = items.filter((item) => item.status === \"done\").length;\n  const active = items.find((item) => item.status === \"in-progress\");\n\n  function move(id: string, direction: -1 | 1) {\n    const index = items.findIndex((item) => item.id === id);\n    const targetIndex = index + direction;\n    if (index === -1 || targetIndex < 0 || targetIndex >= items.length) return;\n    if (items[index].status !== \"pending\" || items[targetIndex].status !== \"pending\") return;\n\n    const reordered = items.map((item) => item.id);\n    [reordered[index], reordered[targetIndex]] = [reordered[targetIndex], reordered[index]];\n    onReorder?.(reordered);\n  }\n\n  return (\n    <motion.section\n      data-slot=\"plan-checklist\"\n      initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={reduceMotion ? spring.quick.exit : spring.moderate.enter}\n      className={cn(\"shadow-bevel w-full max-w-md rounded-lg bg-card px-4 py-3.5\", className)}\n    >\n      <span id={statusId} role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {active\n          ? `${title}: ${doneCount} of ${items.length} done, currently ${active.label}.`\n          : `${title}: ${doneCount} of ${items.length} done.`}\n      </span>\n\n      <div className=\"flex items-center gap-2\">\n        <h3 id={titleId} className=\"text-control text-foreground\">\n          {title}\n        </h3>\n        <span className=\"font-mono text-micro text-muted-foreground\">\n          {doneCount}/{items.length}\n        </span>\n      </div>\n\n      <ol aria-labelledby={titleId} className=\"mt-1 flex flex-col\">\n        <AnimatePresence initial={false}>\n          {items.map((item, index) => {\n            const editable = item.status === \"pending\";\n            const canMoveUp = editable && index > 0 && items[index - 1].status === \"pending\";\n            const canMoveDown =\n              editable && index < items.length - 1 && items[index + 1].status === \"pending\";\n\n            return (\n              <motion.li\n                key={item.id}\n                layout={!reduceMotion}\n                initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{\n                  opacity: 0,\n                  transition: reduceMotion ? { duration: 0 } : spring.quick.exit,\n                }}\n                transition={reduceMotion ? { duration: 0 } : spring.moderate.enter}\n                className=\"group/item flex items-start gap-2.5 py-1.5\"\n              >\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"flex min-w-0 items-center gap-2.5\">\n                    <PlanChecklistStatus\n                      status={item.status}\n                      reduceMotion={Boolean(reduceMotion)}\n                    />\n                    <PlanChecklistLabel\n                      item={item}\n                      editable={editable && Boolean(onRelabel)}\n                      reduceMotion={Boolean(reduceMotion)}\n                      onRelabel={onRelabel}\n                    />\n                  </div>\n                  {item.detail ? (\n                    <p className=\"pl-[1.625rem] text-caption text-muted-foreground\">\n                      {item.detail}\n                    </p>\n                  ) : null}\n                  <span className=\"sr-only\">{STATUS_LABEL[item.status]}</span>\n                </div>\n\n                {onReorder && editable ? (\n                  <div className=\"flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity duration-quick group-hover/item:opacity-100 group-focus-within/item:opacity-100\">\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"icon-xs\"\n                      disabled={!canMoveUp}\n                      onClick={() => move(item.id, -1)}\n                      aria-label={`Move \"${item.label}\" up`}\n                    >\n                      <ChevronUpIcon className=\"size-3.5\" />\n                    </Button>\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"icon-xs\"\n                      disabled={!canMoveDown}\n                      onClick={() => move(item.id, 1)}\n                      aria-label={`Move \"${item.label}\" down`}\n                    >\n                      <ChevronDownIcon className=\"size-3.5\" />\n                    </Button>\n                  </div>\n                ) : null}\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n      </ol>\n    </motion.section>\n  );\n}\n\nexport { PlanChecklist };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}