{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge-text",
  "title": "Gauge Text",
  "description": "An animated stat that counts up to a number next to a gauge icon, like \"89x faster\".",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/gauge-text.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Gauge } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type GaugeSpeed = \"slow\" | \"normal\" | \"fast\";\n\nexport interface GaugeTextProps extends Omit<\n  React.ComponentPropsWithoutRef<\"span\">,\n  \"children\" | \"color\"\n> {\n  ref?: React.Ref<GaugeTextRef>;\n  value: number;\n\n  suffix?: string;\n\n  label: string;\n\n  speed?: GaugeSpeed;\n\n  color?: string;\n\n  landedClassName?: string;\n\n  decimals?: number;\n\n  duration?: number;\n\n  showIcon?: boolean;\n\n  className?: string;\n  numberClassName?: string;\n  labelClassName?: string;\n  iconClassName?: string;\n\n  autoStart?: boolean;\n  inView?: boolean;\n  once?: boolean;\n\n  onStart?: () => void;\n  onComplete?: () => void;\n}\n\nexport interface GaugeTextRef {\n  play: () => void;\n  reset: () => void;\n}\n\nconst COUNT_DURATION: Record<GaugeSpeed, number> = {\n  slow: 2.2,\n  normal: 1.3,\n  fast: 0.75,\n};\n\nfunction easeOutExpo(t: number) {\n  return t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);\n}\n\nfunction GaugeText({\n  ref,\n  value,\n  suffix = \"x\",\n  label,\n  speed = \"normal\",\n  color = \"var(--primary)\",\n  landedClassName = \"text-foreground\",\n  decimals = 0,\n  duration,\n  showIcon = true,\n  className,\n  numberClassName,\n  labelClassName,\n  iconClassName,\n  autoStart = true,\n  inView = false,\n  once = true,\n  onStart,\n  onComplete,\n  ...props\n}: GaugeTextProps) {\n  const [display, setDisplay] = React.useState(0);\n  const [landed, setLanded] = React.useState(false);\n\n  const frame = React.useRef<number | null>(null);\n  const hasPlayedRef = React.useRef(false);\n  const callbacks = React.useRef({ onStart, onComplete });\n  React.useEffect(() => {\n    callbacks.current = { onStart, onComplete };\n  }, [onStart, onComplete]);\n\n  const countDuration = duration ?? COUNT_DURATION[speed];\n\n  const play = React.useCallback(() => {\n    if (frame.current) {\n      cancelAnimationFrame(frame.current);\n    }\n\n    setLanded(false);\n    callbacks.current.onStart?.();\n\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      setDisplay(value);\n      setLanded(true);\n      frame.current = null;\n      callbacks.current.onComplete?.();\n      return;\n    }\n\n    const start = performance.now();\n\n    const tick = (now: number) => {\n      const elapsed = (now - start) / 1000;\n      const t = countDuration > 0 ? Math.min(elapsed / countDuration, 1) : 1;\n      const eased = easeOutExpo(t);\n\n      setDisplay(value * eased);\n\n      if (t < 1) {\n        frame.current = requestAnimationFrame(tick);\n      } else {\n        setDisplay(value);\n        setLanded(true);\n        frame.current = null;\n        callbacks.current.onComplete?.();\n      }\n    };\n\n    frame.current = requestAnimationFrame(tick);\n  }, [value, countDuration]);\n\n  const reset = React.useCallback(() => {\n    if (frame.current) {\n      cancelAnimationFrame(frame.current);\n    }\n\n    setDisplay(0);\n    setLanded(false);\n    hasPlayedRef.current = false;\n  }, []);\n\n  React.useImperativeHandle(ref, () => ({ play, reset }), [play, reset]);\n\n  React.useEffect(() => {\n    let start = 0;\n    if (autoStart && !inView) {\n      hasPlayedRef.current = true;\n      start = requestAnimationFrame(play);\n    }\n\n    return () => {\n      cancelAnimationFrame(start);\n      if (frame.current) {\n        cancelAnimationFrame(frame.current);\n      }\n    };\n  }, [autoStart, inView, play]);\n\n  const handleViewportEnter = React.useCallback(() => {\n    if (once && hasPlayedRef.current) return;\n\n    hasPlayedRef.current = true;\n    play();\n  }, [once, play]);\n\n  const precision = Number.isFinite(decimals)\n    ? Math.max(0, Math.min(100, Math.trunc(decimals)))\n    : 0;\n  const formatted = React.useMemo(() => display.toFixed(precision), [display, precision]);\n\n  return (\n    <span\n      className={cn(\"inline-flex cursor-pointer items-center gap-2 font-medium\", className)}\n      {...props}\n    >\n      <GaugeTrigger inView={inView} once={once} onEnter={handleViewportEnter} />\n\n      <span\n        className={cn(\"tabular-nums\", numberClassName)}\n        style={{\n          color: landed ? undefined : color,\n        }}\n      >\n        {formatted}\n        {suffix}\n      </span>\n\n      {showIcon && (\n        <Gauge\n          className={cn(\"size-[0.72em] shrink-0\", iconClassName)}\n          style={{\n            color: landed ? undefined : color,\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n\n      <span\n        className={cn(landed ? landedClassName : undefined, labelClassName)}\n        style={{\n          color: landed ? undefined : color,\n        }}\n      >\n        {label}\n      </span>\n    </span>\n  );\n}\n\nfunction GaugeTrigger({\n  inView,\n  once,\n  onEnter,\n}: {\n  inView: boolean;\n  once: boolean;\n  onEnter: () => void;\n}) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const firedRef = React.useRef(false);\n\n  React.useEffect(() => {\n    if (!inView || !ref.current) return;\n\n    const node = ref.current;\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry.isIntersecting) return;\n        if (once && firedRef.current) return;\n\n        firedRef.current = true;\n        onEnter();\n      },\n      {\n        rootMargin: \"-10% 0px -10% 0px\",\n      },\n    );\n\n    observer.observe(node);\n\n    return () => observer.disconnect();\n  }, [inView, once, onEnter]);\n\n  if (!inView) return null;\n\n  return <span ref={ref} className=\"sr-only\" aria-hidden=\"true\" />;\n}\n\nexport { GaugeText };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}