{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reasoning-trace","type":"registry:ui","title":"Reasoning Trace","description":"An AI reasoning indicator: the label ripples letter by letter, the current step shimmers past, and a click unfolds the full trace of steps.","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/ui/reasoning-trace.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState, type CSSProperties } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { ChevronDown, Sparkles } from \"lucide-react\";\n\nexport interface ReasoningTraceProps {\n  /** Reasoning steps so far. The last one is the current step while running. */\n  steps?: string[];\n  /** True while the model is still thinking. */\n  running?: boolean;\n  label?: string;\n  /** Shown when finished. `{s}` becomes the elapsed seconds. */\n  doneLabel?: string;\n  /** CSS color of the travelling highlight. */\n  highlight?: string;\n  defaultOpen?: boolean;\n  className?: string;\n}\n\nconst css = `\n@keyframes thought-line-wave{0%,55%,100%{transform:translateY(0);color:var(--tl-base)}20%{transform:translateY(-3px);color:var(--tl-hi)}}\n@keyframes thought-line-sheen{from{background-position:100% 0}to{background-position:0 0}}\n@keyframes thought-line-ping{0%{transform:scale(1);opacity:.7}100%{transform:scale(2.6);opacity:0}}\n.thought-line-char{display:inline-block;white-space:pre;animation:thought-line-wave 1.6s ease-in-out infinite}\n.thought-line-sheen{background:linear-gradient(90deg,var(--tl-base) 40%,var(--tl-hi) 50%,var(--tl-base) 60%) 0 0/250% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:thought-line-sheen 2.2s linear infinite}\n.thought-line-ping{animation:thought-line-ping 1.4s ease-out infinite}\n@media (prefers-reduced-motion:reduce){.thought-line-char,.thought-line-sheen,.thought-line-ping{animation:none}.thought-line-sheen{color:var(--tl-base)}}\n`;\n\nexport function ReasoningTrace({\n  steps = [],\n  running = true,\n  label = \"Thinking\",\n  doneLabel = \"Thought for {s}s\",\n  highlight = \"var(--foreground)\",\n  defaultOpen = false,\n  className,\n}: ReasoningTraceProps) {\n  const reduce = useReducedMotion();\n  const panelId = useId();\n  const [open, setOpen] = useState(defaultOpen);\n  const [seconds, setSeconds] = useState(0);\n  const started = useRef(0);\n  const current = steps[steps.length - 1];\n\n  useEffect(() => {\n    if (running) {\n      started.current = Date.now();\n      return;\n    }\n    if (started.current) setSeconds(Math.max(1, Math.round((Date.now() - started.current) / 1000)));\n  }, [running]);\n\n  const chars = [...label, \".\", \".\", \".\"];\n\n  return (\n    <div\n      className={`w-full max-w-md text-sm ${className ?? \"\"}`}\n      style={{ \"--tl-base\": \"var(--muted-foreground)\", \"--tl-hi\": highlight } as CSSProperties}\n    >\n      <style href=\"reasoning-trace\" precedence=\"default\">\n        {css}\n      </style>\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        aria-controls={panelId}\n        onClick={() => setOpen((o) => !o)}\n        className=\"group flex max-w-full items-center gap-2 rounded-md py-1 pr-1 text-left text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <motion.span\n          className=\"grid shrink-0 place-items-center\"\n          animate={running && !reduce ? { rotate: [0, 18, -12, 0], scale: [1, 1.15, 0.95, 1] } : { rotate: 0, scale: 1 }}\n          transition={running ? { duration: 1.6, repeat: Infinity, ease: \"easeInOut\" } : { duration: 0.3 }}\n        >\n          <Sparkles aria-hidden className=\"size-4\" style={{ color: running ? highlight : undefined }} />\n        </motion.span>\n        {running ? (\n          <>\n            <span className=\"sr-only\">\n              {label}\n              {current ? `: ${current}` : \"\"}\n            </span>\n            <span aria-hidden className=\"shrink-0 font-medium\">\n              {chars.map((c, i) => (\n                <span key={i} className=\"thought-line-char\" style={{ animationDelay: `${i * 70}ms` }}>\n                  {c}\n                </span>\n              ))}\n            </span>\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              {current && !open && (\n                <motion.span\n                  key={current}\n                  aria-hidden\n                  className=\"thought-line-sheen truncate\"\n                  initial={{ opacity: 0, y: 8, filter: \"blur(4px)\" }}\n                  animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                  exit={{ opacity: 0, y: -8, filter: \"blur(4px)\" }}\n                  transition={{ duration: reduce ? 0 : 0.35 }}\n                >\n                  {current}\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </>\n        ) : (\n          <span className=\"font-medium\">{doneLabel.replace(\"{s}\", String(seconds || Math.max(1, steps.length)))}</span>\n        )}\n        <motion.span\n          aria-hidden\n          className=\"shrink-0\"\n          animate={{ rotate: open ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 400, damping: 18 }}\n        >\n          <ChevronDown className=\"size-4\" />\n        </motion.span>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {open && (\n          <motion.div\n            id={panelId}\n            key=\"panel\"\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 28 }}\n            className=\"overflow-hidden\"\n          >\n            <ol aria-live=\"polite\" className=\"relative mt-2 ml-2 grid gap-2.5 border-l border-border py-1 pl-5\">\n              <AnimatePresence initial={false}>\n                {steps.map((s, i) => {\n                  const live = running && i === steps.length - 1;\n                  return (\n                    <motion.li\n                      key={`${i}-${s}`}\n                      className=\"relative leading-5\"\n                      initial={{ opacity: 0, x: -6, filter: \"blur(4px)\" }}\n                      animate={{ opacity: 1, x: 0, filter: \"blur(0px)\" }}\n                      transition={{ duration: reduce ? 0 : 0.4, ease: \"easeOut\" }}\n                    >\n                      <span\n                        aria-hidden\n                        className=\"absolute top-1.5 -left-[25px] grid size-2 place-items-center rounded-full\"\n                        style={{ background: live ? highlight : \"var(--muted-foreground)\", opacity: live ? 1 : 0.5 }}\n                      >\n                        {live && <span className=\"thought-line-ping absolute inset-0 rounded-full\" style={{ background: highlight }} />}\n                      </span>\n                      <span className={live ? \"thought-line-sheen\" : \"text-muted-foreground\"}>{s}</span>\n                    </motion.li>\n                  );\n                })}\n              </AnimatePresence>\n            </ol>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n"}]}