{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CallChip-JS-TW",
	"title": "CallChip",
	"description": "Tool-call chip whose fill wipes across as a live timer ticks, ending green or shaking red.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "CallChip/CallChip.jsx",
			"content": "'use client';\n\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport {\n  CommandLineIcon,\n  File02Icon,\n  PencilEdit01Icon,\n  RefreshIcon,\n  Search01Icon,\n  Tick02Icon\n} from '@hugeicons/core-free-icons';\n\nconst HOLD_AT = 0.9;\nconst SHAKE = [0, -1, 1, -0.66, 0.66, -0.33, 0];\nconst ICONS = { terminal: CommandLineIcon, file: File02Icon, search: Search01Icon, edit: PencilEdit01Icon };\nconst WORDS = { running: 'running', done: 'done', error: 'failed', idle: 'queued' };\n\nconst fmt = ms => (ms < 10000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`);\nconst reduceMotion = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\nconst glyphOf = s => (s === 'done' ? 'check' : s === 'error' ? 'retry' : 'tool');\n\nexport default function CallChip({\n  icon = 'terminal',\n  name = 'bash',\n  argument = 'npm test',\n  status = 'running',\n  expectedMs = 2500,\n  size = 34,\n  radius = 10,\n  color = 'currentColor',\n  surfaceColor = '#27272a',\n  progressColor = 'currentColor',\n  progressOpacity = 0.08,\n  doneColor = '#22c55e',\n  errorColor = '#ef4444',\n  washOpacity = 0.14,\n  shake = 6,\n  showTimer = true,\n  onRetry,\n  className = '',\n  style\n}) {\n  const rootRef = useRef(null);\n  const fillRef = useRef(null);\n  const timerRef = useRef(null);\n  const mountedRef = useRef(false);\n  const fraction = useRef(0);\n  const clock = useRef({ ms: 0 });\n  const shakeAnim = useRef(null);\n  const statusRef = useRef(status);\n  statusRef.current = status;\n  const [mounted, setMounted] = useState(false);\n  const [pressed, setPressed] = useState(false);\n  const [announce, setAnnounce] = useState('');\n  const roll = useRef({ cur: glyphOf(status), prev: null });\n  if (glyphOf(status) !== roll.current.cur) roll.current = { cur: glyphOf(status), prev: roll.current.cur };\n\n  const setFraction = (f, instant) => {\n    const fill = fillRef.current;\n    if (!fill) return;\n    fraction.current = f;\n    if (instant) fill.style.transition = 'none';\n    fill.style.transform = `scaleX(${f})`;\n    if (instant) {\n      void fill.getBoundingClientRect();\n      fill.style.transition = '';\n    }\n  };\n  const apply = (s, animate) => {\n    if (s === 'running') {\n      shakeAnim.current?.cancel();\n      setFraction(0, true);\n      if (animate) setFraction(HOLD_AT, false);\n    } else if (s === 'done') {\n      setFraction(1, !animate);\n    } else if (s === 'error') {\n      const fill = fillRef.current;\n      const live = fill ? new DOMMatrix(getComputedStyle(fill).transform).a : fraction.current;\n      setFraction(Math.min(1, Math.max(0, live)), true);\n      if (animate && shake > 0 && !reduceMotion() && rootRef.current) {\n        shakeAnim.current = rootRef.current.animate(\n          SHAKE.map(k => ({ transform: `translateX(${k * shake}px)`, easing: 'cubic-bezier(0.77, 0, 0.175, 1)' })),\n          { duration: 450, composite: 'add' }\n        );\n      }\n    } else setFraction(0, true);\n  };\n\n  useEffect(() => {\n    mountedRef.current = true;\n    setMounted(true);\n    apply(statusRef.current, statusRef.current === 'running');\n    return () => {\n      mountedRef.current = false;\n      shakeAnim.current?.cancel();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n  useLayoutEffect(() => {\n    if (mountedRef.current) apply(status, true);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [status]);\n\n  useEffect(() => {\n    const write = ms => {\n      clock.current.ms = ms;\n      if (timerRef.current) timerRef.current.textContent = fmt(ms);\n    };\n    if (status !== 'running') {\n      if ((status === 'idle' || !clock.current.ms) && timerRef.current) timerRef.current.textContent = '—';\n      return undefined;\n    }\n    const startedAt = performance.now();\n    write(0);\n    if (reduceMotion()) {\n      const id = setInterval(() => write(performance.now() - startedAt), 100);\n      return () => {\n        clearInterval(id);\n        write(performance.now() - startedAt);\n      };\n    }\n    let raf = 0;\n    const tick = () => {\n      write(performance.now() - startedAt);\n      raf = requestAnimationFrame(tick);\n    };\n    tick();\n    return () => {\n      cancelAnimationFrame(raf);\n      write(performance.now() - startedAt);\n    };\n  }, [status]);\n  useEffect(() => {\n    const ms = showTimer && clock.current.ms ? Math.round(clock.current.ms) : 0;\n    const when = status === 'done' && ms ? ` in ${ms} ms` : status === 'error' && ms ? ` after ${ms} ms` : '';\n    setAnnounce(`${name} ${argument}, ${WORDS[status] ?? status}${when}`);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [status]);\n\n  const font = Math.max(11, Math.round(size * 0.38));\n  const glyphState = g => (g === roll.current.cur ? 'in' : g === roll.current.prev ? 'out' : undefined);\n  const toolIcon = typeof icon === 'string' ? (ICONS[icon] ?? ICONS.terminal) : null;\n  const iconSize = font + 2;\n\n  return (\n    <span\n      ref={rootRef}\n      role=\"status\"\n      aria-busy={status === 'running' || undefined}\n      data-status={status}\n      data-mounted={mounted ? '' : undefined}\n      data-pressed={pressed ? '' : undefined}\n      className={`group relative box-border inline-flex items-center overflow-hidden leading-none whitespace-nowrap select-none [-webkit-touch-callout:none] [height:var(--cc-size)] [padding:0_var(--cc-pad)] [gap:var(--cc-gap)] [border-radius:var(--cc-radius)] [background:var(--cc-surface)] [color:var(--cc-color)] [font-size:var(--cc-font)] [transition:transform_160ms_cubic-bezier(0.23,1,0.32,1)] data-[status=error]:cursor-pointer data-[pressed]:scale-[0.97] motion-reduce:data-[pressed]:scale-100${className ? ` ${className}` : ''}`}\n      style={{\n        '--cc-size': `${size}px`,\n        '--cc-font': `${font}px`,\n        '--cc-pad': `${Math.round(size * 0.35)}px`,\n        '--cc-gap': `${Math.round(font * 0.55)}px`,\n        '--cc-radius': `${radius}px`,\n        '--cc-color': color,\n        '--cc-surface': surfaceColor,\n        '--cc-progress': progressColor,\n        '--cc-progress-pct': `${progressOpacity * 100}%`,\n        '--cc-done': doneColor,\n        '--cc-error': errorColor,\n        '--cc-wash-pct': `${washOpacity * 100}%`,\n        '--cc-expected': `${expectedMs}ms`,\n        ...style\n      }}\n    >\n      <span\n        ref={fillRef}\n        className=\"pointer-events-none absolute inset-0 origin-left scale-x-0 [clip-path:inset(0_0_0_0)] [background:color-mix(in_srgb,var(--cc-progress)_var(--cc-progress-pct),transparent)] group-data-[status=running]:[transition:transform_var(--cc-expected)_linear] group-data-[status=done]:[clip-path:inset(100%_0_0_0)] group-data-[status=done]:[background:color-mix(in_srgb,var(--cc-done)_var(--cc-wash-pct),transparent)] group-data-[status=done]:[transition:transform_200ms_cubic-bezier(0.23,1,0.32,1),background-color_120ms_ease,clip-path_400ms_cubic-bezier(0.23,1,0.32,1)_200ms] group-data-[status=error]:[background:color-mix(in_srgb,var(--cc-error)_var(--cc-wash-pct),transparent)] group-data-[status=error]:[transition:background-color_200ms_ease] motion-reduce:group-data-[status=done]:[clip-path:inset(0_0_0_0)] motion-reduce:group-data-[status=done]:[transition:background-color_120ms_ease] group-not-data-[mounted]:transition-none!\"\n        aria-hidden=\"true\"\n      />\n      <span\n        className=\"relative flex-none overflow-hidden [width:calc(var(--cc-font)+2px)] [height:calc(var(--cc-font)+2px)]\"\n        aria-hidden=\"true\"\n      >\n        <span\n          className=\"absolute inset-0 grid place-items-center opacity-0 blur-[3px] [transform:translateY(70%)] data-[state=in]:opacity-100 data-[state=in]:blur-0 data-[state=in]:[transform:none] data-[state=in]:[transition:opacity_240ms_cubic-bezier(0.23,1,0.32,1),transform_240ms_cubic-bezier(0.23,1,0.32,1),filter_240ms_cubic-bezier(0.23,1,0.32,1)] data-[state=out]:[transform:translateY(-70%)] data-[state=out]:[transition:opacity_160ms_cubic-bezier(0.23,1,0.32,1),transform_160ms_cubic-bezier(0.23,1,0.32,1),filter_160ms_cubic-bezier(0.23,1,0.32,1)] group-data-[status=done]:data-[state=in]:[color:var(--cc-done)] group-data-[status=error]:data-[state=in]:[color:var(--cc-error)] motion-reduce:[transform:none]! motion-reduce:[filter:none]! group-not-data-[mounted]:transition-none!\"\n          data-state={glyphState('tool')}\n        >\n          {toolIcon ? <HugeiconsIcon icon={toolIcon} size={iconSize} strokeWidth={1.8} /> : icon}\n        </span>\n        <span\n          className=\"absolute inset-0 grid place-items-center opacity-0 blur-[3px] [transform:translateY(70%)] data-[state=in]:opacity-100 data-[state=in]:blur-0 data-[state=in]:[transform:none] data-[state=in]:[transition:opacity_240ms_cubic-bezier(0.23,1,0.32,1),transform_240ms_cubic-bezier(0.23,1,0.32,1),filter_240ms_cubic-bezier(0.23,1,0.32,1)] data-[state=out]:[transform:translateY(-70%)] data-[state=out]:[transition:opacity_160ms_cubic-bezier(0.23,1,0.32,1),transform_160ms_cubic-bezier(0.23,1,0.32,1),filter_160ms_cubic-bezier(0.23,1,0.32,1)] group-data-[status=done]:data-[state=in]:[color:var(--cc-done)] group-data-[status=error]:data-[state=in]:[color:var(--cc-error)] motion-reduce:[transform:none]! motion-reduce:[filter:none]! group-not-data-[mounted]:transition-none!\"\n          data-state={glyphState('check')}\n        >\n          <HugeiconsIcon icon={Tick02Icon} size={iconSize} strokeWidth={2.2} />\n        </span>\n        <span\n          className=\"absolute inset-0 grid place-items-center opacity-0 blur-[3px] [transform:translateY(70%)] data-[state=in]:opacity-100 data-[state=in]:blur-0 data-[state=in]:[transform:none] data-[state=in]:[transition:opacity_240ms_cubic-bezier(0.23,1,0.32,1),transform_240ms_cubic-bezier(0.23,1,0.32,1),filter_240ms_cubic-bezier(0.23,1,0.32,1)] data-[state=out]:[transform:translateY(-70%)] data-[state=out]:[transition:opacity_160ms_cubic-bezier(0.23,1,0.32,1),transform_160ms_cubic-bezier(0.23,1,0.32,1),filter_160ms_cubic-bezier(0.23,1,0.32,1)] group-data-[status=done]:data-[state=in]:[color:var(--cc-done)] group-data-[status=error]:data-[state=in]:[color:var(--cc-error)] motion-reduce:[transform:none]! motion-reduce:[filter:none]! group-not-data-[mounted]:transition-none!\"\n          data-state={glyphState('retry')}\n        >\n          <HugeiconsIcon icon={RefreshIcon} size={iconSize} strokeWidth={2} />\n        </span>\n      </span>\n      <span className=\"relative font-medium\" aria-hidden=\"true\">\n        {name}\n      </span>\n      <span className=\"relative opacity-[0.72]\" aria-hidden=\"true\">\n        {argument}\n      </span>\n      {showTimer ? (\n        <span ref={timerRef} className=\"relative min-w-[6ch] text-right tabular-nums opacity-50\" aria-hidden=\"true\">\n          0 ms\n        </span>\n      ) : null}\n      {status === 'error' && onRetry ? (\n        <button\n          type=\"button\"\n          className=\"absolute -inset-[5px] m-0 cursor-pointer touch-manipulation appearance-none border-0 bg-transparent p-0 outline-none [border-radius:var(--cc-radius)] [-webkit-tap-highlight-color:transparent]\"\n          aria-label={`Retry ${name} ${argument}`}\n          onClick={() => onRetry()}\n          onPointerDown={() => setPressed(true)}\n          onPointerUp={() => setPressed(false)}\n          onPointerCancel={() => setPressed(false)}\n        />\n      ) : null}\n      <span className=\"sr-only\">{announce}</span>\n    </span>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}