{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-progress",
  "title": "Agent Progress",
  "description": "Collapsible multi-step progress with a circular indicator on the active task.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/button.json",
    "https://components.exe.xyz/r/progress.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/blocks/agent-progress.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Progress } from \"@/components/ui/progress\"\n\nexport function AgentProgress({\n  steps,\n  stepDuration = 2400,\n  completionDelay = 600,\n  onFinished,\n  defaultExpanded = true,\n  className,\n}: {\n  steps: string[]\n  stepDuration?: number\n  completionDelay?: number\n  onFinished?: () => void\n  defaultExpanded?: boolean\n  className?: string\n}) {\n  const [expanded, setExpanded] = React.useState(defaultExpanded)\n  const [visibleCount, setVisibleCount] = React.useState(0)\n  const [activeIndex, setActiveIndex] = React.useState(0)\n  const [completedCount, setCompletedCount] = React.useState(0)\n  const [percent, setPercent] = React.useState(0)\n  const [tween, setTween] = React.useState(false)\n  const [done, setDone] = React.useState(false)\n  const [reduced, setReduced] = React.useState(false)\n  const onFinishedRef = React.useRef(onFinished)\n  onFinishedRef.current = onFinished\n\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    setReduced(mq.matches)\n    const onChange = () => setReduced(mq.matches)\n    mq.addEventListener(\"change\", onChange)\n    return () => mq.removeEventListener(\"change\", onChange)\n  }, [])\n\n  React.useEffect(() => {\n    setVisibleCount(0)\n    setActiveIndex(0)\n    setCompletedCount(0)\n    setPercent(0)\n    setDone(false)\n    setTween(false)\n  }, [steps])\n\n  React.useEffect(() => {\n    if (reduced) {\n      setVisibleCount(steps.length)\n      return\n    }\n    let shown = 0\n    const id = window.setInterval(() => {\n      shown += 1\n      setVisibleCount(shown)\n      if (shown >= steps.length) window.clearInterval(id)\n    }, 70)\n    return () => window.clearInterval(id)\n  }, [steps, reduced])\n\n  const ready = visibleCount >= steps.length && steps.length > 0\n\n  React.useEffect(() => {\n    if (!ready || done) return\n    setTween(false)\n    setPercent(reduced ? 100 : 0)\n    const start = requestAnimationFrame(() => {\n      setTween(!reduced)\n      setPercent(100)\n    })\n    const timer = window.setTimeout(\n      () => {\n        setCompletedCount((count) => count + 1)\n        if (activeIndex >= steps.length - 1) {\n          setDone(true)\n        } else {\n          setActiveIndex((index) => index + 1)\n          setPercent(0)\n          setTween(false)\n        }\n      },\n      reduced ? 0 : stepDuration\n    )\n    return () => {\n      cancelAnimationFrame(start)\n      window.clearTimeout(timer)\n    }\n  }, [ready, done, activeIndex, steps.length, stepDuration, reduced])\n\n  React.useEffect(() => {\n    if (!done) return\n    const id = window.setTimeout(\n      () => onFinishedRef.current?.(),\n      completionDelay\n    )\n    return () => window.clearTimeout(id)\n  }, [done, completionDelay])\n\n  const remaining = done ? 0 : Math.max(steps.length - completedCount, 0)\n  const header = remaining === 0 ? \"Done\" : `${remaining} steps left`\n  const overall = steps.length\n    ? Math.round(\n        ((completedCount + (done ? 0 : percent / 100)) / steps.length) * 100\n      )\n    : 0\n\n  return (\n    <section\n      data-slot=\"agent-progress\"\n      className={cn(\n        \"w-full max-w-md rounded-[2px] border border-border bg-card p-3\",\n        className\n      )}\n      style={{ [\"--step-ms\" as string]: `${stepDuration}ms` }}\n    >\n      <div className=\"flex items-center gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon-xs\"\n          aria-expanded={expanded}\n          aria-label={expanded ? \"Collapse steps\" : \"Expand steps\"}\n          onClick={() => setExpanded((open) => !open)}\n        >\n          <ChevronDown\n            className={cn(\n              \"size-3.5 transition-transform duration-150\",\n              !expanded && \"-rotate-90\"\n            )}\n          />\n        </Button>\n        <h2\n          className=\"min-w-0 flex-1 font-heading text-sm font-medium\"\n          aria-live=\"polite\"\n        >\n          {header}\n        </h2>\n        <span className=\"font-mono text-[11px] text-muted-foreground\">\n          {overall}%\n        </span>\n      </div>\n      {!expanded ? (\n        <Progress value={overall} className=\"mt-3\" />\n      ) : (\n        <ol className=\"mt-3 flex flex-col gap-1.5\">\n          {steps.map((step, index) => {\n            if (index >= visibleCount) return null\n            const complete = index < completedCount\n            const active = !done && index === activeIndex && ready\n            const status = complete ? \"complete\" : active ? \"active\" : \"pending\"\n            return (\n              <li\n                key={`${index}-${step}`}\n                aria-current={active ? \"step\" : undefined}\n                className={cn(\n                  \"flex items-center gap-2 rounded-[2px] px-1 py-1 text-sm\",\n                  active && \"bg-muted/60\",\n                  !reduced &&\n                    \"motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1\"\n                )}\n                style={\n                  reduced\n                    ? undefined\n                    : {\n                        animationDelay: `${index * 70}ms`,\n                        animationFillMode: \"both\",\n                      }\n                }\n              >\n                <StepGlyph\n                  status={status}\n                  percent={active ? percent : complete ? 100 : 0}\n                  tween={tween && active}\n                  durationMs={stepDuration}\n                />\n                <span\n                  className={cn(\n                    \"min-w-0 flex-1\",\n                    status === \"pending\" && \"text-muted-foreground\",\n                    status === \"complete\" && \"text-muted-foreground\"\n                  )}\n                >\n                  {step}\n                </span>\n              </li>\n            )\n          })}\n        </ol>\n      )}\n    </section>\n  )\n}\n\nfunction StepGlyph({\n  status,\n  percent,\n  tween,\n  durationMs,\n}: {\n  status: \"pending\" | \"active\" | \"complete\"\n  percent: number\n  tween: boolean\n  durationMs: number\n}) {\n  const radius = 7\n  const circumference = 2 * Math.PI * radius\n  if (status === \"complete\") {\n    return (\n      <svg\n        viewBox=\"0 0 20 20\"\n        className=\"size-5 shrink-0 text-primary\"\n        aria-hidden\n      >\n        <circle cx=\"10\" cy=\"10\" r=\"8\" fill=\"currentColor\" />\n        <path\n          d=\"M6.2 10.4 8.8 13l5-6.2\"\n          fill=\"none\"\n          stroke=\"var(--primary-foreground)\"\n          strokeWidth=\"1.7\"\n          strokeLinecap=\"square\"\n        />\n      </svg>\n    )\n  }\n  if (status === \"active\") {\n    return (\n      <svg\n        viewBox=\"0 0 20 20\"\n        className=\"size-5 shrink-0 -rotate-90 text-primary\"\n        aria-hidden\n      >\n        <circle\n          cx=\"10\"\n          cy=\"10\"\n          r={radius}\n          fill=\"none\"\n          stroke=\"var(--border)\"\n          strokeWidth=\"2\"\n        />\n        <circle\n          cx=\"10\"\n          cy=\"10\"\n          r={radius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeDasharray={circumference}\n          strokeDashoffset={circumference * (1 - percent / 100)}\n          strokeLinecap=\"butt\"\n          style={{\n            transition: tween\n              ? `stroke-dashoffset ${durationMs}ms linear`\n              : \"none\",\n          }}\n        />\n      </svg>\n    )\n  }\n  return (\n    <svg\n      viewBox=\"0 0 20 20\"\n      className=\"size-5 shrink-0 text-muted-foreground\"\n      aria-hidden\n    >\n      <circle\n        cx=\"10\"\n        cy=\"10\"\n        r=\"7\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}