{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-animated-number",
  "title": "AnimatedNumber",
  "description": "Contador numérico animado com formatação por locale e fallback sr-only para leitores de tela.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@lema-ds/ui-i18n",
    "@lema-ds/ds-locale-provider"
  ],
  "files": [
    {
      "path": "components/ds/ds-animated-number.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { UILocale } from \"@/lib/ui-i18n\"\nimport { useUILocale } from \"@/components/ui/ds-locale-provider\"\n\n// ── Types ──\n\nexport interface AnimatedNumberProps\n  extends\n    Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\">,\n    VariantProps<typeof animatedNumberVariants> {\n  value: number\n  /** Animation duration in ms. */\n  duration?: number\n  decimals?: number\n  locale?: UILocale\n  prefix?: string\n  suffix?: string\n}\n\n// ── Variants ──\n\nexport const animatedNumberVariants = cva(\n  \"inline-block font-semibold text-foreground tabular-nums\",\n  {\n    variants: {\n      size: {\n        sm: \"text-xs\",\n        md: \"text-sm\",\n        lg: \"text-base\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\n// ── Component ──\n\n/**\n * Counts up/down to `value` on mount and whenever it changes. Renders the\n * animated digits as decorative (`aria-hidden`) and exposes the final,\n * static value once via a `sr-only` span so assistive tech doesn't hear\n * every intermediate frame. Jumps straight to the final value under\n * `prefers-reduced-motion`.\n */\nexport function AnimatedNumber({\n  value,\n  duration = 800,\n  decimals = 0,\n  locale: localeProp,\n  prefix = \"\",\n  suffix = \"\",\n  size = \"md\",\n  className,\n  ...props\n}: AnimatedNumberProps) {\n  const locale = useUILocale(localeProp)\n  const [display, setDisplay] = React.useState(0)\n  const fromRef = React.useRef(0)\n\n  React.useEffect(() => {\n    const from = fromRef.current\n    const delta = value - from\n    if (delta === 0) return\n\n    const reducedMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches\n    const effectiveDuration = reducedMotion ? 0 : duration\n\n    let raf = 0\n    const start = performance.now()\n\n    const tick = (now: number) => {\n      const progress =\n        effectiveDuration === 0\n          ? 1\n          : Math.min((now - start) / effectiveDuration, 1)\n      const eased = 1 - Math.pow(1 - progress, 3)\n      setDisplay(from + delta * eased)\n      if (progress < 1) {\n        raf = requestAnimationFrame(tick)\n      } else {\n        fromRef.current = value\n      }\n    }\n\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [value, duration])\n\n  const format = React.useCallback(\n    (n: number) =>\n      new Intl.NumberFormat(locale, {\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(n),\n    [locale, decimals]\n  )\n\n  return (\n    <span\n      data-slot=\"animated-number\"\n      className={cn(animatedNumberVariants({ size }), className)}\n      {...props}\n    >\n      <span aria-hidden=\"true\">\n        {prefix}\n        {format(display)}\n        {suffix}\n      </span>\n      <span className=\"sr-only\">\n        {prefix}\n        {format(value)}\n        {suffix}\n      </span>\n    </span>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
