{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "count-up",
  "type": "registry:ui",
  "title": "Count Up",
  "description": "A number that counts to its value when it scrolls into view, without a render per frame.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/count-up.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface CountUpProps extends Omit<\n  React.ComponentProps<\"span\">,\n  \"children\"\n> {\n  /** The number to land on. */\n  value: number\n  /** The number to start from. */\n  from?: number\n  /** Milliseconds for the whole count. */\n  duration?: number\n  /** Decimal places to hold throughout the count. */\n  decimals?: number\n  /** Locale passed to Intl.NumberFormat, so grouping matches the reader. */\n  locale?: string\n  /** Rendered before the number, inside the same element. */\n  prefix?: string\n  /** Rendered after the number. */\n  suffix?: string\n  /** Hold until the number scrolls into view instead of counting on mount. */\n  startOnView?: boolean\n  /** Render the final value with no count. */\n  disabled?: boolean\n}\n\n/** --ease-out-quart, expressed as a function so JS and CSS agree. */\nfunction easeOutQuart(t: number) {\n  return 1 - Math.pow(1 - t, 4)\n}\n\nexport function CountUp({\n  value,\n  from = 0,\n  duration = 1400,\n  decimals = 0,\n  locale,\n  prefix,\n  suffix,\n  startOnView = true,\n  disabled = false,\n  className,\n  ...props\n}: CountUpProps) {\n  const ref = React.useRef<HTMLSpanElement>(null)\n  const [active, setActive] = React.useState(!startOnView)\n\n  const format = React.useMemo(\n    () =>\n      new Intl.NumberFormat(locale, {\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }),\n    [locale, decimals]\n  )\n\n  React.useEffect(() => {\n    if (!startOnView || disabled) {\n      return\n    }\n\n    const node = ref.current\n    if (!node || typeof IntersectionObserver === \"undefined\") {\n      setActive(true)\n      return\n    }\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          setActive(true)\n          observer.disconnect()\n        }\n      },\n      { threshold: 0.4 }\n    )\n\n    observer.observe(node)\n    return () => observer.disconnect()\n  }, [startOnView, disabled])\n\n  React.useEffect(() => {\n    const node = ref.current\n    if (!node) {\n      return\n    }\n\n    const reduced =\n      typeof window !== \"undefined\" &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n    if (disabled || !active || reduced || duration <= 0) {\n      node.textContent = format.format(value)\n      return\n    }\n\n    let frame = 0\n    let start: number | null = null\n\n    // The value is written straight to the DOM rather than through state, so a\n    // 1400ms count is one render instead of eighty-four.\n    const tick = (now: number) => {\n      start ??= now\n      const progress = Math.min((now - start) / duration, 1)\n      node.textContent = format.format(\n        from + (value - from) * easeOutQuart(progress)\n      )\n\n      if (progress < 1) {\n        frame = requestAnimationFrame(tick)\n      }\n    }\n\n    node.textContent = format.format(from)\n    frame = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(frame)\n  }, [active, disabled, duration, format, from, value])\n\n  return (\n    <span data-slot=\"count-up\" className={cn(\"tabular-nums\", className)}>\n      {prefix}\n      <span ref={ref} {...props}>\n        {format.format(disabled ? value : from)}\n      </span>\n      {suffix}\n    </span>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}