{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan",
  "title": "Plan",
  "description": "A plan an agent asks to run, with approve, reject and auto-run countdown.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/button.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/plan.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CircleDashedIcon, CheckIcon, MinusIcon, MoreHorizontalIcon } from \"lucide-react\";\n\nimport { enterChildren, enterOnce, marksIn, motionScale } from \"@/lib/motion\";\nimport { Button } from \"@/registry/cuelark/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype PlanStep = { label: string; done?: boolean };\n\ntype PlanProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  title?: React.ReactNode;\n  steps: PlanStep[];\n  onApprove?: () => void;\n  onReject?: () => void;\n  /** Run automatically after this many seconds unless cancelled (a ring counts down on the Run button). */\n  autoRun?: number;\n  /** Steps shown before \"N more\". */\n  visible?: number;\n  /** Bind ⌘/Ctrl+Enter to run and Esc to reject while the plan is pending. */\n  shortcuts?: boolean;\n  /** Start already decided, e.g. when rendering a past turn from history. */\n  defaultState?: \"pending\" | \"approved\" | \"rejected\";\n};\n\n/**\n * A plan an agent wants to execute, held for your go-ahead. Crop marks lock onto it as it arrives; long plans fold\n * after a few steps; Run can count down to auto-approve (cancel by pressing it's ring). Resolved plans settle quietly.\n */\nfunction Plan({ title = \"Plan\", steps, onApprove, onReject, autoRun, visible = 4, shortcuts = true, defaultState = \"pending\", className, ...props }: PlanProps) {\n  const [state, setState] = React.useState(defaultState);\n  const [expanded, setExpanded] = React.useState(false);\n  const [left, setLeft] = React.useState(autoRun ?? 0);\n  const [counting, setCounting] = React.useState(!!autoRun && defaultState === \"pending\");\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  const approve = React.useCallback(() => {\n    setState(\"approved\");\n    setCounting(false);\n    onApprove?.();\n  }, [onApprove]);\n  const reject = React.useCallback(() => {\n    setState(\"rejected\");\n    setCounting(false);\n    onReject?.();\n  }, [onReject]);\n\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    enterOnce(el, (s) => ({ opacity: 0, y: 10 * s }), { duration: 0.4 });\n    if (el && motionScale(el)) {\n      marksIn(el, 0.1);\n      const steps = el.querySelector(\"ol\");\n      if (steps) enterChildren(steps.children, { opacity: 0, x: -6 }, { stagger: 0.06, delay: 0.15, duration: 0.3 });\n    }\n  }, []);\n\n  React.useEffect(() => {\n    if (!counting || state !== \"pending\") return;\n    if (left <= 0) return approve();\n    const id = setTimeout(() => setLeft((l) => Math.max(0, l - 0.1)), 100);\n    return () => clearTimeout(id);\n  }, [counting, left, state, approve]);\n\n  React.useEffect(() => {\n    if (!shortcuts || state !== \"pending\") return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) approve();\n      if (e.key === \"Escape\") reject();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [shortcuts, state, approve, reject]);\n\n  const hidden = Math.max(0, steps.length - visible);\n  const shown = expanded ? steps : steps.slice(0, visible);\n  const pending = state === \"pending\";\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"plan\"\n      data-state={state}\n      className={cn(\"relative grid gap-3 rounded-2xl border border-border bg-card p-4 shadow-raised\", pending && \"cue-marks\", className)}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <span className=\"cue-label\">{title}</span>\n        <span className=\"font-mono text-xs text-muted-foreground tabular-nums\">\n          {steps.filter((s) => s.done).length}/{steps.length}\n        </span>\n      </div>\n      <ol className=\"grid gap-2\">\n        {shown.map((s, i) => (\n          <li key={`${s.label}-${i}`} className=\"flex items-center gap-2.5 text-sm\">\n            {s.done ? (\n              <span className=\"grid size-4 place-items-center rounded-full bg-foreground text-background\">\n                <CheckIcon className=\"size-2.5\" strokeWidth={3.5} />\n              </span>\n            ) : (\n              <CircleDashedIcon className=\"size-4 shrink-0 text-muted-foreground/60\" />\n            )}\n            <span className={cn(\"truncate\", !s.done && \"text-muted-foreground\")}>{s.label}</span>\n          </li>\n        ))}\n        {hidden > 0 && (\n          <li>\n            <button type=\"button\" onClick={() => setExpanded((e) => !e)} aria-expanded={expanded} className=\"flex items-center gap-2.5 rounded-md text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50\">\n              {expanded ? <MinusIcon className=\"size-4\" /> : <MoreHorizontalIcon className=\"size-4\" />}\n              {expanded ? \"Show less\" : `${hidden} more`}\n            </button>\n          </li>\n        )}\n      </ol>\n      {pending ? (\n        <div className=\"flex items-center justify-end gap-2 border-t border-border pt-3\">\n          <Button variant=\"ghost\" size=\"sm\" onClick={reject} kbd={shortcuts ? \"esc\" : undefined}>\n            Reject\n          </Button>\n          <Button size=\"sm\" onClick={approve} kbd={shortcuts ? \"⌘⏎\" : undefined}>\n            {counting && autoRun ? <CountdownRing left={left} total={autoRun} onCancel={() => setCounting(false)} /> : null}\n            Run plan\n          </Button>\n        </div>\n      ) : (\n        <p className={cn(\"flex items-center gap-2 border-t border-border pt-3 text-sm\", state === \"approved\" ? \"text-foreground\" : \"text-muted-foreground\")}>\n          <span className={cn(\"size-1.5 rounded-full\", state === \"approved\" ? \"bg-signal\" : \"bg-muted-foreground\")} />\n          {state === \"approved\" ? \"Approved, running\" : \"Rejected\"}\n        </p>\n      )}\n    </div>\n  );\n}\n\nfunction CountdownRing({ left, total, onCancel }: { left: number; total: number; onCancel: () => void }) {\n  const r = 6;\n  const c = 2 * Math.PI * r;\n  return (\n    <span\n      role=\"button\"\n      tabIndex={0}\n      aria-label={`Auto-run in ${Math.ceil(left)} seconds; cancel`}\n      onClick={(e) => {\n        e.stopPropagation();\n        onCancel();\n      }}\n      onKeyDown={(e) => (e.key === \"Enter\" || e.key === \" \") && (e.stopPropagation(), e.preventDefault(), onCancel())}\n      className=\"relative after:absolute after:-inset-2 -ms-1 grid size-4 place-items-center rounded-full\"\n    >\n      <svg viewBox=\"0 0 16 16\" className=\"size-4 -rotate-90\" aria-hidden=\"true\">\n        <circle cx=\"8\" cy=\"8\" r={r} fill=\"none\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n        <circle cx=\"8\" cy=\"8\" r={r} fill=\"none\" stroke=\"var(--signal)\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeDasharray={c} strokeDashoffset={c * (1 - left / total)} className=\"transition-[stroke-dashoffset] duration-100 ease-linear\" />\n      </svg>\n    </span>\n  );\n}\n\nexport { Plan };\nexport type { PlanProps, PlanStep };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/plan",
  "categories": [
    "agents"
  ],
  "type": "registry:ui"
}