{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "animated-number",
  "title": "Animated Number",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/animated-number/AnimatedNumber.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface AnimatedNumberProps extends HTMLAttributes<HTMLSpanElement> {\n    value: number\n    /** Value the first animation starts from. */\n    from?: number\n    /** ms per tween. */\n    duration?: number\n    /** ms before the first tween starts. */\n    delay?: number\n    format?: (value: number) => string\n    /** Render the target value instantly, no tween. */\n    disabled?: boolean\n    ref?: HTMLSpanElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy, onMount, untrack } from 'svelte'\n  import { cn } from '$lib/utils'\n\n  /**\n   * Tweened number display. Counts up on mount, then smoothly retargets\n   * from the currently displayed value whenever `value` changes.\n   * Server render outputs the final value so hydration always matches.\n   */\n  let {\n    value,\n    from = 0,\n    duration = 900,\n    delay = 0,\n    format,\n    disabled = false,\n    class: className,\n    ref = $bindable(null),\n    ...restProps\n  }: AnimatedNumberProps = $props()\n\n  const defaultFormat = (v: number) => String(Math.round(v))\n\n  // Snapshot once: the server render outputs the final value so hydration\n  // matches; the mount tween below takes over from there.\n  const getInitialDisplay = () => value\n  let display = $state(getInitialDisplay())\n\n  let raf = 0\n  let startTimer: number | undefined\n\n  function cancel() {\n    if (raf) cancelAnimationFrame(raf)\n    raf = 0\n    if (startTimer !== undefined) {\n      clearTimeout(startTimer)\n      startTimer = undefined\n    }\n  }\n\n  function tweenTo(target: number, animateFrom: number, withDelay: boolean) {\n    cancel()\n    const reduce = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    if (disabled || reduce) {\n      display = target\n      return\n    }\n    const startTime = performance.now()\n    const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3)\n    const run = () => {\n      const t = Math.min(1, (performance.now() - startTime) / duration)\n      display = animateFrom + (target - animateFrom) * easeOutCubic(t)\n      raf = t < 1 ? requestAnimationFrame(run) : 0\n    }\n    if (withDelay && delay > 0) {\n      startTimer = window.setTimeout(() => {\n        startTimer = undefined\n        run()\n      }, delay)\n    } else {\n      run()\n    }\n  }\n\n  onMount(() => {\n    // First paint showed the final value (SSR-safe); snap to `from`, then animate.\n    tweenTo(value, from, true)\n  })\n\n  let firstRun = true\n  $effect(() => {\n    const next = value\n    // The mount tween above owns the first run; this effect only retargets\n    // on later changes. `display` is read untracked so tween frames don't\n    // resubscribe and restart the tween.\n    if (firstRun) {\n      firstRun = false\n      return\n    }\n    const animateFrom = untrack(() => display)\n    tweenTo(next, animateFrom, false)\n  })\n\n  onDestroy(cancel)\n\n  const formatted = $derived((format ?? defaultFormat)(display))\n</script>\n\n<span bind:this={ref} data-uipkge=\"\" data-slot=\"animated-number\" class={cn('tabular-nums', className)} {...restProps}>\n  {formatted}\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/animated-number/AnimatedNumber.svelte"
    },
    {
      "path": "packages/registry-svelte/components/animated-number/index.ts",
      "content": "export { default as AnimatedNumber, type AnimatedNumberProps } from './AnimatedNumber.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/animated-number/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Tweened number display — counts up on mount and smoothly retargets when the value changes. Ease-out cubic via requestAnimationFrame, custom formatter (Intl-ready), delay/stagger support, honors prefers-reduced-motion, SSR-safe.",
  "categories": [
    "display",
    "data"
  ]
}