{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ThoughtLine-TS-TW",
	"title": "ThoughtLine",
	"description": "Reasoning trace header with a live clock that folds its steps into one line when done.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "ThoughtLine/ThoughtLine.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport { animate, useReducedMotion, type AnimationPlaybackControls } from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { ArrowDown01Icon, SparklesIcon, Tick02Icon } from '@hugeicons/core-free-icons';\nexport type ThoughtLineGlyph = 'sparkle' | 'dot' | 'none' | ReactNode;\n\nexport interface ThoughtLineProps {\n  label?: string;\n  doneLabel?: string;\n  renderLabel?: (text: string, working: boolean) => ReactNode;\n  glyph?: ThoughtLineGlyph;\n  steps?: string[];\n  collapsible?: boolean;\n  collapseOnSettle?: boolean;\n  color?: string;\n  glyphColor?: string;\n  fontSize?: number;\n  breathPeriod?: number;\n  breathDepth?: number;\n  shimmer?: boolean;\n  shimmerDuration?: number;\n  settleDuration?: number;\n  settleBlur?: number;\n  working?: boolean;\n  settleAfter?: number;\n  elapsed?: number;\n  showTimer?: boolean;\n  onSettle?: (seconds: number) => void;\n  className?: string;\n  style?: CSSProperties;\n}\n\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\nconst EASE_IN_OUT: [number, number, number, number] = [0.77, 0, 0.175, 1];\nconst GLYPH_DONE = 0.55;\nconst EMPTY_STEPS: string[] = [];\n\nconst fmt = (ds: number) =>\n  ds < 600 ? `${(ds / 10).toFixed(1)}s` : `${Math.floor(ds / 600)}m ${((ds % 600) / 10).toFixed(1)}s`;\nconst spoken = (ds: number) =>\n  ds < 600\n    ? `${(ds / 10).toFixed(1)} seconds`\n    : `${Math.floor(ds / 600)} minutes ${((ds % 600) / 10).toFixed(1)} seconds`;\n\nconst STYLE = `\n@keyframes thought-line-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }\n@keyframes thought-line-shimmer { to { background-position: -125% 0; } }\n`;\n\nconst ThoughtLine: React.FC<ThoughtLineProps> = ({\n  label = 'Thinking…',\n  doneLabel = '',\n  renderLabel,\n  glyph = 'sparkle',\n  steps = EMPTY_STEPS,\n  collapsible = true,\n  collapseOnSettle = true,\n  color = 'currentColor',\n  glyphColor = '',\n  fontSize = 16,\n  breathPeriod = 1.6,\n  breathDepth = 0.45,\n  shimmer = true,\n  shimmerDuration = 1.8,\n  settleDuration = 350,\n  settleBlur = 2,\n  working = true,\n  settleAfter = 0,\n  elapsed,\n  showTimer = true,\n  onSettle,\n  className = '',\n  style\n}) => {\n  const reduce = useReducedMotion();\n  const [autoSettled, setAutoSettled] = useState(false);\n  const [open, setOpen] = useState(true);\n  const isWorking = working && !autoSettled;\n  const doneText = doneLabel || (showTimer ? 'Thought for' : 'Done thinking');\n  const hasTrace = steps.length > 0;\n  const depth = reduce ? Math.min(breathDepth, 0.2) : breathDepth;\n  const period = reduce ? breathPeriod * 1.5 : breathPeriod;\n  const trough = 1 - depth;\n  const sheen = shimmer && !reduce;\n\n  const glyphRef = useRef<HTMLSpanElement>(null);\n  const breathRef = useRef<HTMLSpanElement>(null);\n  const timerRef = useRef<HTMLSpanElement>(null);\n  const stackRef = useRef<HTMLSpanElement>(null);\n  const workRef = useRef<HTMLSpanElement>(null);\n  const doneRef = useRef<HTMLSpanElement>(null);\n  const dsRef = useRef(0);\n  const prevWorking = useRef(isWorking);\n  const latest = useRef<{ onSettle?: (seconds: number) => void }>({});\n  latest.current = { onSettle };\n  const [announce, setAnnounce] = useState(label);\n\n  useEffect(() => {\n    if (working) setAutoSettled(false);\n  }, [working]);\n  useEffect(() => {\n    if (isWorking) setOpen(true);\n    else if (collapseOnSettle) setOpen(false);\n  }, [isWorking, collapseOnSettle]);\n\n  useEffect(() => {\n    const glyphEl = glyphRef.current;\n    const breathEl = breathRef.current;\n    if (!breathEl) return undefined;\n    const s = settleDuration / 1000;\n    const loop = (el: HTMLElement, delay: number) =>\n      animate(el, { opacity: [trough, 1, trough] }, { duration: period, ease: EASE_IN_OUT, repeat: Infinity, delay });\n    let cancelled = false;\n    const running: AnimationPlaybackControls[] = [];\n    if (isWorking) {\n      if (depth > 0) {\n        if (sheen) running.push(animate(breathEl, { opacity: 1 }, { duration: 0.2, ease: EASE_OUT }));\n        if (glyphEl) {\n          const lead = animate(glyphEl, { opacity: trough }, { duration: 0.2, ease: EASE_OUT });\n          running.push(lead);\n          lead.then(() => {\n            if (cancelled) return;\n            running.push(loop(glyphEl, 0));\n            if (!sheen) running.push(loop(breathEl, 0.14));\n          });\n        } else if (!sheen) {\n          running.push(loop(breathEl, 0.14));\n        }\n      } else {\n        if (glyphEl) running.push(animate(glyphEl, { opacity: 1 }, { duration: 0.2, ease: EASE_OUT }));\n        running.push(animate(breathEl, { opacity: 1 }, { duration: 0.2, ease: EASE_OUT }));\n      }\n    } else {\n      if (glyphEl) running.push(animate(glyphEl, { opacity: GLYPH_DONE }, { duration: s, ease: EASE_OUT }));\n      running.push(animate(breathEl, { opacity: 1 }, { duration: s, ease: EASE_OUT }));\n    }\n    return () => {\n      cancelled = true;\n      running.forEach(a => a.stop());\n    };\n  }, [isWorking, period, depth, trough, settleDuration, glyph, sheen]);\n\n  const paint = (ds: number) => {\n    dsRef.current = ds;\n    if (timerRef.current) timerRef.current.textContent = fmt(ds);\n  };\n  useLayoutEffect(() => {\n    if (elapsed != null) {\n      paint(Math.round(elapsed * 10));\n      return undefined;\n    }\n    if (!isWorking) return undefined;\n    const startedAt = performance.now();\n    paint(0);\n    const id = setInterval(() => {\n      const ds = Math.floor((performance.now() - startedAt) / 100);\n      paint(ds);\n      if (settleAfter > 0 && ds >= Math.round(settleAfter * 10)) setAutoSettled(true);\n    }, 100);\n    return () => clearInterval(id);\n  }, [isWorking, elapsed, settleAfter]);\n\n  useLayoutEffect(() => {\n    const t = timerRef.current;\n    const stack = stackRef.current;\n    if (!t || !stack) return undefined;\n    const place = (glide: boolean) => {\n      const active = isWorking ? workRef.current : doneRef.current;\n      if (!active) return;\n      const shift = active.offsetWidth - stack.offsetWidth;\n      if (!glide) t.style.transition = 'none';\n      t.style.transform = `translateX(${shift}px)`;\n      if (!glide) {\n        void t.offsetWidth;\n        t.style.transition = '';\n      }\n    };\n    place(prevWorking.current !== isWorking);\n    prevWorking.current = isWorking;\n    const ro = new ResizeObserver(() => place(false));\n    if (workRef.current) ro.observe(workRef.current);\n    if (doneRef.current) ro.observe(doneRef.current);\n    return () => ro.disconnect();\n  }, [isWorking, label, doneText, fontSize, showTimer]);\n\n  useEffect(() => {\n    if (isWorking) {\n      setAnnounce(label);\n      return;\n    }\n    setAnnounce(showTimer ? `${doneText} ${spoken(dsRef.current)}` : doneText);\n    latest.current.onSettle?.(dsRef.current / 10);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isWorking]);\n\n  const toggle = hasTrace && collapsible;\n  const head = (\n    <>\n      {glyph !== 'none' ? (\n        <span\n          ref={glyphRef}\n          className=\"mr-[0.2em] inline-flex h-[1.1em] w-[1.1em] flex-none [color:var(--tl-glyph)] [&>svg]:block [&>svg]:h-full [&>svg]:w-full\"\n          aria-hidden=\"true\"\n        >\n          {glyph === 'sparkle' ? (\n            <HugeiconsIcon icon={SparklesIcon} size=\"100%\" strokeWidth={2} />\n          ) : glyph === 'dot' ? (\n            <span className=\"m-auto h-[0.5em] w-[0.5em] rounded-full bg-current\" />\n          ) : (\n            glyph\n          )}\n        </span>\n      ) : null}\n      <span ref={stackRef} className=\"inline-grid\" aria-hidden=\"true\">\n        <span\n          ref={workRef}\n          className=\"w-max opacity-0 [grid-area:1/1] [filter:blur(var(--tl-blur))] [transition:opacity_var(--tl-settle)_cubic-bezier(0.23,1,0.32,1),filter_var(--tl-settle)_cubic-bezier(0.23,1,0.32,1)] data-[active]:opacity-100 data-[active]:[filter:blur(0)] motion-reduce:[filter:none]! motion-reduce:[transition:opacity_var(--tl-settle)_ease]\"\n          data-active={isWorking ? '' : undefined}\n        >\n          <span\n            ref={breathRef}\n            className=\"inline-block group-data-[working]:data-[shimmer]:bg-clip-text group-data-[working]:data-[shimmer]:text-transparent group-data-[working]:data-[shimmer]:[background-image:linear-gradient(100deg,color-mix(in_srgb,var(--tl-color)_50%,transparent)_30%,var(--tl-color)_50%,color-mix(in_srgb,var(--tl-color)_50%,transparent)_70%)] group-data-[working]:data-[shimmer]:[background-size:250%_100%] group-data-[working]:data-[shimmer]:[background-position:125%_0] group-data-[working]:data-[shimmer]:[-webkit-text-fill-color:transparent] group-data-[working]:data-[shimmer]:[animation:thought-line-shimmer_var(--tl-shimmer)_linear_infinite]\"\n            data-shimmer={sheen ? '' : undefined}\n          >\n            {renderLabel ? renderLabel(label, true) : label}\n          </span>\n        </span>\n        <span\n          ref={doneRef}\n          className=\"w-max opacity-0 [grid-area:1/1] [filter:blur(var(--tl-blur))] [transition:opacity_var(--tl-settle)_cubic-bezier(0.23,1,0.32,1),filter_var(--tl-settle)_cubic-bezier(0.23,1,0.32,1)] data-[active]:[opacity:var(--tl-done)] data-[active]:[filter:blur(0)] motion-reduce:[filter:none]! motion-reduce:[transition:opacity_var(--tl-settle)_ease]\"\n          data-active={isWorking ? undefined : ''}\n        >\n          {renderLabel ? renderLabel(doneText, false) : doneText}\n        </span>\n      </span>\n      {showTimer ? (\n        <span\n          ref={timerRef}\n          className=\"tabular-nums [opacity:var(--tl-timer)] [transition:opacity_var(--tl-settle)_ease,transform_var(--tl-settle)_cubic-bezier(0.77,0,0.175,1)] data-[done]:[opacity:var(--tl-done)] motion-reduce:[transition:opacity_var(--tl-settle)_ease]\"\n          data-done={isWorking ? undefined : ''}\n          aria-hidden=\"true\"\n        >\n          0.0s\n        </span>\n      ) : null}\n      {collapsible ? (\n        <span\n          className=\"ml-[0.1em] inline-flex opacity-0 [transition:opacity_200ms_ease,transform_200ms_cubic-bezier(0.23,1,0.32,1)] data-[on]:opacity-55 group-data-[open]:rotate-180\"\n          data-on={hasTrace ? '' : undefined}\n          aria-hidden=\"true\"\n        >\n          <HugeiconsIcon icon={ArrowDown01Icon} size=\"1em\" strokeWidth={2.2} />\n        </span>\n      ) : null}\n      <span className=\"sr-only\" role=\"status\">\n        {announce}\n      </span>\n    </>\n  );\n\n  return (\n    <div\n      className={`group inline-flex flex-col items-start leading-[1.2] font-medium [color:var(--tl-color)] [font-family:inherit] [font-size:var(--tl-font)]${className ? ` ${className}` : ''}`}\n      data-working={isWorking ? '' : undefined}\n      data-open={open && hasTrace ? '' : undefined}\n      style={\n        {\n          '--tl-font': `${fontSize}px`,\n          '--tl-color': color,\n          '--tl-glyph': glyphColor || color,\n          '--tl-settle': `${settleDuration}ms`,\n          '--tl-blur': `${settleBlur}px`,\n          '--tl-shimmer': `${shimmerDuration}s`,\n          ...style\n        } as CSSProperties\n      }\n    >\n      <style>{STYLE}</style>\n      {collapsible ? (\n        <button\n          type=\"button\"\n          className=\"relative m-0 inline-flex cursor-default items-center gap-[0.3em] border-0 bg-transparent p-0 text-left whitespace-nowrap text-inherit outline-none [font:inherit] data-[toggle]:cursor-pointer data-[toggle]:[-webkit-tap-highlight-color:transparent]\"\n          data-toggle={toggle ? '' : undefined}\n          aria-expanded={toggle ? open : undefined}\n          tabIndex={toggle ? 0 : -1}\n          onClick={() => {\n            if (toggle) setOpen(v => !v);\n          }}\n        >\n          {head}\n        </button>\n      ) : (\n        <div className=\"relative m-0 inline-flex cursor-default items-center gap-[0.3em] border-0 bg-transparent p-0 text-left whitespace-nowrap text-inherit outline-none [font:inherit] data-[toggle]:cursor-pointer data-[toggle]:[-webkit-tap-highlight-color:transparent]\">\n          {head}\n        </div>\n      )}\n      {hasTrace ? (\n        <div\n          className=\"grid w-0 min-w-full [grid-template-rows:0fr] [transition:grid-template-rows_var(--tl-settle)_cubic-bezier(0.23,1,0.32,1)] data-[open]:[grid-template-rows:1fr]\"\n          data-open={open ? '' : undefined}\n          aria-hidden={!open}\n        >\n          <div className=\"min-h-0 overflow-x-visible overflow-y-clip\">\n            <div className=\"flex w-max flex-col gap-[0.45em] pt-[0.6em] pb-[0.2em] pl-[1.6em] text-[0.875em] font-normal whitespace-nowrap\">\n              {steps.map((text, i) => {\n                const done = !isWorking || i < steps.length - 1;\n                return (\n                  <div\n                    key={`${i}-${text}`}\n                    className=\"group/step flex translate-y-0 items-center gap-[0.5em] opacity-100 [transition:opacity_200ms_cubic-bezier(0.23,1,0.32,1),transform_200ms_cubic-bezier(0.23,1,0.32,1)] starting:-translate-y-1 starting:opacity-0 motion-reduce:[transform:none]! motion-reduce:[transition:opacity_200ms_ease]\"\n                    data-done={done ? '' : undefined}\n                  >\n                    <span\n                      className=\"inline-grid h-[1em] w-[1em] flex-none place-items-center opacity-55\"\n                      aria-hidden=\"true\"\n                    >\n                      {done ? (\n                        <HugeiconsIcon icon={Tick02Icon} size=\"1em\" strokeWidth={2.5} />\n                      ) : (\n                        <i className=\"block h-[0.4em] w-[0.4em] rounded-full bg-current [animation:thought-line-pulse_1.6s_cubic-bezier(0.77,0,0.175,1)_infinite] motion-reduce:[animation-duration:2.4s]\" />\n                      )}\n                    </span>\n                    <span className=\"[transition:opacity_var(--tl-settle)_ease] group-data-[done]/step:opacity-55\">\n                      {text}\n                    </span>\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\nexport default ThoughtLine;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}