{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-animated-number",
  "title": "Use Animated Number",
  "description": "A hook for animating a number.",
  "registryDependencies": ["https://dashboardblocks.com/r/use-in-view.json"],
  "files": [
    {
      "path": "registry/hooks/use-animated-number.tsx",
      "content": "'use client'\n\nimport { useInView } from '@/registry/hooks/use-in-view'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\ntype EasingFunction = (t: number) => number\n\nconst easings: Record<string, EasingFunction> = {\n  linear: (t) => t,\n  easeOut: (t) => 1 - Math.pow(1 - t, 3),\n  easeIn: (t) => t * t * t,\n  easeInOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),\n  spring: (t) => 1 - Math.pow(Math.cos(t * Math.PI * 0.5), 3),\n}\n\ninterface UseAnimatedNumberOptions {\n  /**\n   * Duration of the animation in milliseconds\n   * @default 1000\n   */\n  duration?: number\n  /**\n   * Easing function for the animation\n   * @default 'easeOut'\n   */\n  easing?: keyof typeof easings | EasingFunction\n  /**\n   * Starting value for the animation\n   * @default 0\n   */\n  from?: number\n  /**\n   * Intersection Observer threshold (0-1)\n   * @default 0.1\n   */\n  threshold?: number\n  /**\n   * Intersection Observer root margin\n   * @default '0px'\n   */\n  rootMargin?: string\n  /**\n   * Delay before starting animation in milliseconds\n   * @default 0\n   */\n  delay?: number\n  /**\n   * Whether to re-animate when element comes back into view\n   * @default false\n   */\n  reanimateOnView?: boolean\n}\n\nexport function useAnimatedNumber(to: number, options: UseAnimatedNumberOptions = {}) {\n  const {\n    delay = 0,\n    duration = 1000,\n    easing = 'easeOut',\n    from = 0,\n    reanimateOnView = false,\n    rootMargin = '0px',\n    threshold = 1.0,\n  } = options\n\n  const [value, setValue] = useState(from)\n  const [isAnimating, setIsAnimating] = useState(false)\n  const hasAnimated = useRef(false)\n  const animationRef = useRef<number | null>(null)\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n  const { isInView, ref } = useInView({ rootMargin, threshold })\n\n  const decimals = to.toString().split('.')[1]?.length || 0\n\n  const easingFn =\n    typeof easing === 'function' ? easing : easings[easing] || easings.easeOut\n\n  const animate = useCallback(() => {\n    if (animationRef.current) {\n      cancelAnimationFrame(animationRef.current)\n    }\n\n    const startTime = performance.now()\n    const startValue = from\n\n    setIsAnimating(true)\n\n    const tick = (currentTime: number) => {\n      const elapsed = currentTime - startTime\n      const progress = Math.min(elapsed / duration, 1)\n      const easedProgress = easingFn(progress)\n      const currentValue = startValue + (to - startValue) * easedProgress\n\n      setValue(\n        decimals > 0 ? Number(currentValue.toFixed(decimals)) : Math.round(currentValue),\n      )\n\n      if (progress < 1) {\n        animationRef.current = requestAnimationFrame(tick)\n      } else {\n        setIsAnimating(false)\n        animationRef.current = null\n      }\n    }\n\n    animationRef.current = requestAnimationFrame(tick)\n  }, [from, to, duration, easingFn, decimals])\n\n  const reset = useCallback(() => {\n    if (animationRef.current) {\n      cancelAnimationFrame(animationRef.current)\n      animationRef.current = null\n    }\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n      timeoutRef.current = null\n    }\n    setValue(from)\n    setIsAnimating(false)\n    hasAnimated.current = false\n  }, [from])\n\n  const startAnimation = useCallback(() => {\n    if (delay > 0) {\n      timeoutRef.current = setTimeout(animate, delay)\n    } else {\n      animate()\n    }\n  }, [animate, delay])\n\n  // Trigger animation when element comes into view\n  useEffect(() => {\n    if (isInView) {\n      if (!hasAnimated.current || reanimateOnView) {\n        // Use requestAnimationFrame to defer state updates and avoid lint errors\n        requestAnimationFrame(() => {\n          if (reanimateOnView) {\n            setValue(from)\n          }\n          startAnimation()\n          hasAnimated.current = true\n        })\n      }\n    }\n  }, [isInView, reanimateOnView, from, startAnimation])\n\n  // Cleanup animations and timeouts on unmount\n  useEffect(() => {\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n      }\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current)\n      }\n    }\n  }, [])\n\n  return {\n    animate,\n    isAnimating,\n    isInView,\n    ref,\n    reset,\n    value,\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}
