{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CallChip-JS-CSS",
	"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:file",
			"path": "CallChip.css",
			"target": "@components/CallChip.css",
			"content": ".call-chip {\n  --cc-size: 34px;\n  --cc-font: 13px;\n  --cc-pad: 12px;\n  --cc-gap: 7px;\n  --cc-radius: 10px;\n  --cc-color: currentColor;\n  --cc-surface: #27272a;\n  --cc-progress: currentColor;\n  --cc-progress-pct: 8%;\n  --cc-done: #22c55e;\n  --cc-error: #ef4444;\n  --cc-wash-pct: 14%;\n  --cc-expected: 2500ms;\n  --cc-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  gap: var(--cc-gap);\n  box-sizing: border-box;\n  height: var(--cc-size);\n  padding: 0 var(--cc-pad);\n  border-radius: var(--cc-radius);\n  overflow: hidden;\n  background: var(--cc-surface);\n  color: var(--cc-color);\n  font-size: var(--cc-font);\n  line-height: 1;\n  white-space: nowrap;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  transition: transform 160ms var(--cc-ease-out);\n}\n\n.call-chip[data-status='error'] {\n  cursor: pointer;\n}\n\n.call-chip[data-pressed] {\n  transform: scale(0.97);\n}\n\n.call-chip__fill {\n  position: absolute;\n  inset: 0;\n  background: color-mix(in srgb, var(--cc-progress) var(--cc-progress-pct), transparent);\n  transform: scaleX(0);\n  transform-origin: left center;\n  clip-path: inset(0 0 0 0);\n  pointer-events: none;\n}\n\n.call-chip[data-status='running'] .call-chip__fill {\n  transition: transform var(--cc-expected) linear;\n}\n\n.call-chip[data-status='done'] .call-chip__fill {\n  background: color-mix(in srgb, var(--cc-done) var(--cc-wash-pct), transparent);\n  clip-path: inset(100% 0 0 0);\n  transition:\n    transform 200ms var(--cc-ease-out),\n    background-color 120ms ease,\n    clip-path 400ms var(--cc-ease-out) 200ms;\n}\n\n.call-chip[data-status='error'] .call-chip__fill {\n  background: color-mix(in srgb, var(--cc-error) var(--cc-wash-pct), transparent);\n  transition: background-color 200ms ease;\n}\n\n.call-chip:not([data-mounted]) .call-chip__fill,\n.call-chip:not([data-mounted]) .call-chip__glyph {\n  transition: none !important;\n}\n\n.call-chip__slot {\n  position: relative;\n  flex: none;\n  width: calc(var(--cc-font) + 2px);\n  height: calc(var(--cc-font) + 2px);\n  overflow: hidden;\n}\n\n.call-chip__glyph {\n  position: absolute;\n  inset: 0;\n  display: grid;\n  place-items: center;\n  opacity: 0;\n  transform: translateY(70%);\n  filter: blur(3px);\n}\n\n.call-chip__glyph[data-state='in'] {\n  opacity: 1;\n  transform: none;\n  filter: blur(0);\n  transition:\n    opacity 240ms var(--cc-ease-out),\n    transform 240ms var(--cc-ease-out),\n    filter 240ms var(--cc-ease-out);\n}\n\n.call-chip__glyph[data-state='out'] {\n  transform: translateY(-70%);\n  transition:\n    opacity 160ms var(--cc-ease-out),\n    transform 160ms var(--cc-ease-out),\n    filter 160ms var(--cc-ease-out);\n}\n\n.call-chip[data-status='done'] .call-chip__glyph[data-state='in'] {\n  color: var(--cc-done);\n}\n\n.call-chip[data-status='error'] .call-chip__glyph[data-state='in'] {\n  color: var(--cc-error);\n}\n\n.call-chip__name,\n.call-chip__arg,\n.call-chip__timer {\n  position: relative;\n}\n\n.call-chip__name {\n  font-weight: 500;\n}\n\n.call-chip__arg {\n  opacity: 0.72;\n}\n\n.call-chip__timer {\n  min-width: 6ch;\n  font-variant-numeric: tabular-nums;\n  text-align: right;\n  opacity: 0.5;\n}\n\n.call-chip__retry {\n  position: absolute;\n  inset: -5px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  border-radius: var(--cc-radius);\n  background: transparent;\n  appearance: none;\n  cursor: pointer;\n  outline: none;\n  touch-action: manipulation;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.call-chip__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .call-chip[data-status='done'] .call-chip__fill {\n    clip-path: inset(0 0 0 0);\n    transition: background-color 120ms ease;\n  }\n\n  .call-chip__glyph {\n    transform: none !important;\n    filter: none !important;\n  }\n\n  .call-chip[data-pressed] {\n    transform: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "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\nimport './CallChip.css';\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={`call-chip${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 ref={fillRef} className=\"call-chip__fill\" aria-hidden=\"true\" />\n      <span className=\"call-chip__slot\" aria-hidden=\"true\">\n        <span className=\"call-chip__glyph\" data-state={glyphState('tool')}>\n          {toolIcon ? <HugeiconsIcon icon={toolIcon} size={iconSize} strokeWidth={1.8} /> : icon}\n        </span>\n        <span className=\"call-chip__glyph\" data-state={glyphState('check')}>\n          <HugeiconsIcon icon={Tick02Icon} size={iconSize} strokeWidth={2.2} />\n        </span>\n        <span className=\"call-chip__glyph\" data-state={glyphState('retry')}>\n          <HugeiconsIcon icon={RefreshIcon} size={iconSize} strokeWidth={2} />\n        </span>\n      </span>\n      <span className=\"call-chip__name\" aria-hidden=\"true\">\n        {name}\n      </span>\n      <span className=\"call-chip__arg\" aria-hidden=\"true\">\n        {argument}\n      </span>\n      {showTimer ? (\n        <span ref={timerRef} className=\"call-chip__timer\" aria-hidden=\"true\">\n          0 ms\n        </span>\n      ) : null}\n      {status === 'error' && onRetry ? (\n        <button\n          type=\"button\"\n          className=\"call-chip__retry\"\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=\"call-chip__sr\">{announce}</span>\n    </span>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}