{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "working-out-loud",
  "type": "registry:component",
  "title": "Working Out Loud",
  "description": "Lets people watch an agent think: a running clock, steps that tick off along a rail, sources that slide in as they're found, and a tidy one-line summary once it's done.",
  "categories": [
    "component",
    "Conversation"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/working-out-loud.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ChevronDownIcon, GlobeIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Stays false until mounted so the server render and the first client render match.\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport interface WorkingSource {\n  title: string;\n  domain: string;\n}\n\nexport interface WorkingStep {\n  label: string;\n  duration?: number;\n  sources?: WorkingSource[];\n}\n\nexport interface WorkingOutLoudProps {\n  title?: string;\n  steps?: WorkingStep[];\n  loop?: boolean;\n  defaultOpen?: boolean;\n  className?: string;\n}\n\nconst defaultSteps: WorkingStep[] = [\n  { label: \"Understanding the question\", duration: 1400 },\n  {\n    label: \"Searching the web\",\n    duration: 3600,\n    sources: [\n      { title: \"Heat pump sizing guide\", domain: \"northwind.energy\" },\n      { title: \"Cold-climate efficiency study\", domain: \"fjordlabs.org\" },\n      { title: \"Installer pricing survey 2026\", domain: \"hearthly.com\" },\n      { title: \"Rebate programs by region\", domain: \"civicgrid.gov\" },\n    ],\n  },\n  { label: \"Comparing running costs\", duration: 2200 },\n  { label: \"Writing the answer\", duration: 1800 },\n];\n\nconst spring = { type: \"spring\", stiffness: 380, damping: 32 } as const;\n\nconst easeOut = [0.23, 1, 0.32, 1] as const;\nconst tints = [\"bg-sky-500\", \"bg-emerald-500\", \"bg-orange-500\", \"bg-violet-500\", \"bg-rose-500\"];\n\nexport function WorkingOutLoud({\n  title = \"Thinking\",\n  steps = defaultSteps,\n  loop = false,\n  defaultOpen = true,\n  className,\n}: WorkingOutLoudProps) {\n  const reduce = useSafeReducedMotion();\n  const panelId = useId();\n  const [open, setOpen] = useState(defaultOpen);\n  const [active, setActive] = useState(0);\n  const [revealed, setRevealed] = useState(0);\n  const [elapsed, setElapsed] = useState(0);\n  const startRef = useRef<number>(0);\n  const done = active >= steps.length;\n\n  useEffect(() => {\n    if (done) return;\n    if (active === 0 && revealed === 0) startRef.current = performance.now();\n    const t = setInterval(() => setElapsed(performance.now() - startRef.current), 100);\n    return () => clearInterval(t);\n  }, [done, active, revealed]);\n\n  useEffect(() => {\n    if (done) {\n      if (!loop) return;\n      const t = setTimeout(() => {\n        setActive(0);\n        setRevealed(0);\n        setElapsed(0);\n        startRef.current = performance.now();\n      }, 4200);\n      return () => clearTimeout(t);\n    }\n    const step = steps[active];\n    const duration = step?.duration ?? 1600;\n    const sources = step?.sources ?? [];\n    if (sources.length && revealed < sources.length) {\n      const t = setTimeout(() => setRevealed((r) => r + 1), duration / (sources.length + 1));\n      return () => clearTimeout(t);\n    }\n    const remaining = sources.length ? duration / (sources.length + 1) : duration;\n    const t = setTimeout(() => {\n      setActive((a) => a + 1);\n      setRevealed(0);\n    }, remaining);\n    return () => clearTimeout(t);\n  }, [active, revealed, done, loop, steps]);\n\n  const seconds = (elapsed / 1000).toFixed(1);\n  const sourceCount = steps.reduce(\n    (n, s, i) => n + (i < active ? (s.sources?.length ?? 0) : i === active ? revealed : 0),\n    0,\n  );\n\n  return (\n    <div className={cn(\"w-full max-w-md rounded-2xl border bg-card text-sm shadow-xs/5\", className)}>\n      <button\n        aria-controls={panelId}\n        aria-expanded={open}\n        className=\"group/head flex w-full cursor-pointer items-center gap-3 rounded-2xl px-4 py-3 text-start outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        onClick={() => setOpen((o) => !o)}\n        type=\"button\"\n      >\n        <PulseDot done={done} />\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"relative block h-5 overflow-hidden font-medium\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n                className=\"block truncate\"\n                exit={{ y: -12, opacity: 0, filter: \"blur(2px)\" }}\n                initial={{ y: 12, opacity: 0, filter: \"blur(2px)\" }}\n                key={done ? \"done\" : \"working\"}\n                transition={{ duration: reduce ? 0 : 0.25, ease: easeOut }}\n              >\n                {done ? `Thought for ${seconds}s` : title}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n          <span className=\"block truncate text-muted-foreground text-xs\">\n            {done\n              ? `${steps.length} steps · ${sourceCount} sources`\n              : (steps[active]?.label ?? \"\")}\n          </span>\n        </span>\n        {!done && (\n          <span aria-label={`${seconds} seconds elapsed`} className=\"min-w-[5ch] text-end font-mono text-muted-foreground text-xs tabular-nums\">\n            {seconds}s\n          </span>\n        )}\n        <motion.span\n          animate={{ rotate: open ? 180 : 0 }}\n          className=\"grid size-6 place-items-center rounded-md text-muted-foreground transition-colors duration-150 group-hover/head:bg-accent group-hover/head:text-foreground\"\n          transition={{ duration: reduce ? 0 : 0.2, ease: easeOut }}\n        >\n          <ChevronDownIcon className=\"size-4\" />\n        </motion.span>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {open && (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            id={panelId}\n            initial={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : spring}\n          >\n            <ol className=\"relative mx-4 mb-4 border-t pt-3\">\n              {steps.map((step, i) => {\n                const state = i < active ? \"done\" : i === active ? \"active\" : \"pending\";\n                const shown = i < active ? (step.sources?.length ?? 0) : i === active ? revealed : 0;\n                return (\n                  <li className=\"relative flex gap-3 pb-3 last:pb-0\" key={step.label}>\n                    {i < steps.length - 1 && (\n                      <span aria-hidden className=\"absolute start-[8.5px] top-[22px] bottom-px w-px bg-border\">\n                        <motion.span\n                          animate={{ scaleY: i < active ? 1 : 0 }}\n                          className=\"absolute inset-0 origin-top bg-success\"\n                          initial={false}\n                          transition={spring}\n                        />\n                      </span>\n                    )}\n                    <StepMark state={state} />\n                    <div className=\"min-w-0 flex-1\">\n                      <p\n                        className={cn(\n                          \"leading-5 transition-colors duration-300\",\n                          state === \"pending\" ? \"text-muted-foreground\" : \"text-foreground\",\n                          state === \"done\" && \"text-muted-foreground\",\n                        )}\n                      >\n                        {step.label}\n                      </p>\n                      {step.sources && step.sources.length > 0 && (\n                        <ul className=\"flex flex-col\">\n                          <AnimatePresence initial={false}>\n                            {step.sources.slice(0, shown).map((src, si) => (\n                              <motion.li\n                                animate={{ height: \"auto\", opacity: 1 }}\n                                className=\"overflow-hidden\"\n                                initial={reduce ? false : { height: 0, opacity: 0 }}\n                                key={src.domain}\n                                transition={{ height: spring, opacity: { duration: 0.2, ease: \"easeOut\" } }}\n                              >\n                                <motion.div\n                                  animate={{ y: 0, filter: \"blur(0px)\" }}\n                                  className=\"mt-1.5 flex items-center gap-2 rounded-lg bg-muted/60 px-2 py-1.5\"\n                                  initial={reduce ? false : { y: 6, filter: \"blur(2px)\" }}\n                                  transition={{ duration: 0.3, ease: easeOut }}\n                                >\n                                <span\n                                  className={cn(\n                                    \"grid size-4 shrink-0 place-items-center rounded-full font-medium text-[9px] text-white uppercase\",\n                                    tints[si % tints.length],\n                                  )}\n                                >\n                                  {src.domain[0]}\n                                </span>\n                                <span className=\"min-w-0 flex-1 truncate text-xs\">{src.title}</span>\n                                <span className=\"flex shrink-0 items-center gap-1 text-muted-foreground text-xs\">\n                                  <GlobeIcon className=\"size-3\" />\n                                  {src.domain}\n                                </span>\n                                </motion.div>\n                              </motion.li>\n                            ))}\n                          </AnimatePresence>\n                        </ul>\n                      )}\n                    </div>\n                  </li>\n                );\n              })}\n            </ol>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction StepMark({ state }: { state: \"done\" | \"active\" | \"pending\" }) {\n  const reduce = useSafeReducedMotion();\n  return (\n    <span className=\"relative z-10 mt-px grid size-4.5 shrink-0 place-items-center\">\n      <span\n        className={cn(\n          \"absolute inset-0 rounded-full border-[1.5px] transition-colors duration-300\",\n          state === \"done\" ? \"border-success bg-success\" : \"bg-card\",\n          state === \"active\" && \"border-border\",\n          state === \"pending\" && \"border-muted-foreground/35\",\n        )}\n      />\n      {state === \"active\" &&\n        (reduce ? (\n          <span className=\"size-1.5 rounded-full bg-foreground\" />\n        ) : (\n          <motion.span\n            animate={{ rotate: 360 }}\n            className=\"absolute inset-0 rounded-full border-[1.5px] border-transparent border-t-foreground\"\n            transition={{ duration: 0.8, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }}\n          />\n        ))}\n      {state === \"done\" && (\n        <motion.svg\n          animate={{ scale: 1 }}\n          aria-hidden\n          className=\"relative size-3 text-white\"\n          fill=\"none\"\n          initial={reduce ? false : { scale: 0.7 }}\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth={3.2}\n          transition={{ duration: 0.2, ease: easeOut }}\n          viewBox=\"0 0 24 24\"\n        >\n          <motion.path\n            animate={{ pathLength: 1 }}\n            d=\"M5 12.5 10 17.5 19 7\"\n            initial={{ pathLength: reduce ? 1 : 0 }}\n            transition={{ duration: 0.28, ease: \"easeOut\" }}\n          />\n        </motion.svg>\n      )}\n    </span>\n  );\n}\n\nfunction PulseDot({ done }: { done: boolean }) {\n  const reduce = useSafeReducedMotion();\n  return (\n    <span aria-hidden className=\"relative grid size-8 shrink-0 place-items-center rounded-full bg-muted\">\n      {!done && !reduce && (\n        <motion.span\n          animate={{ scale: [1, 1.9], opacity: [0.5, 0] }}\n          className=\"absolute inset-1 rounded-full bg-info/40\"\n          transition={{ duration: 1.6, repeat: Number.POSITIVE_INFINITY, ease: \"easeOut\" }}\n        />\n      )}\n      <motion.span\n        animate={{ scale: done && !reduce ? 1.2 : 1 }}\n        className={cn(\"size-2.5 rounded-full transition-colors duration-300\", done ? \"bg-success\" : \"bg-info\")}\n        transition={{ duration: 0.25, ease: easeOut }}\n      />\n    </span>\n  );\n}\n"
    }
  ]
}