{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-texts-sliding-number",
  "title": "Sliding Number",
  "description": "A sliding number animation.",
  "dependencies": [
    "motion",
    "react-use-measure"
  ],
  "registryDependencies": [
    "@odysseyui/hooks-use-is-in-view"
  ],
  "files": [
    {
      "path": "registry/primitives/texts/sliding-number/index.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport {\n  useSpring,\n  useTransform,\n  motion,\n  useMotionValue,\n  type MotionValue,\n  type SpringOptions,\n  type HTMLMotionProps,\n} from 'motion/react';\nimport useMeasure from 'react-use-measure';\n\nimport {\n  useIsInView,\n  type UseIsInViewOptions,\n} from '@/hooks/use-is-in-view';\n\ntype SlidingNumberRollerProps = {\n  prevValue: number;\n  value: number;\n  place: number;\n  transition: SpringOptions;\n  delay?: number;\n};\n\nfunction SlidingNumberRoller({\n  prevValue,\n  value,\n  place,\n  transition,\n  delay = 0,\n}: SlidingNumberRollerProps) {\n  const startNumber = Math.floor(prevValue / place) % 10;\n  const targetNumber = Math.floor(value / place) % 10;\n  const animatedValue = useSpring(startNumber, transition);\n\n  React.useEffect(() => {\n    const timeoutId = setTimeout(() => {\n      animatedValue.set(targetNumber);\n    }, delay);\n    return () => clearTimeout(timeoutId);\n  }, [targetNumber, animatedValue, delay]);\n\n  const [measureRef, { height }] = useMeasure();\n\n  return (\n    <span\n      ref={measureRef}\n      data-slot=\"sliding-number-roller\"\n      style={{\n        position: 'relative',\n        display: 'inline-block',\n        width: '1ch',\n        overflowX: 'visible',\n        overflowY: 'clip',\n        lineHeight: 1,\n        fontVariantNumeric: 'tabular-nums',\n      }}\n    >\n      <span style={{ visibility: 'hidden' }}>0</span>\n      {Array.from({ length: 10 }, (_, i) => (\n        <SlidingNumberDisplay\n          key={i}\n          motionValue={animatedValue}\n          number={i}\n          height={height}\n          transition={transition}\n        />\n      ))}\n    </span>\n  );\n}\n\ntype SlidingNumberDisplayProps = {\n  motionValue: MotionValue<number>;\n  number: number;\n  height: number;\n  transition: SpringOptions;\n};\n\nfunction SlidingNumberDisplay({\n  motionValue,\n  number,\n  height,\n  transition,\n}: SlidingNumberDisplayProps) {\n  const y = useTransform(motionValue, (latest) => {\n    if (!height) return 0;\n    const currentNumber = latest % 10;\n    const offset = (10 + number - currentNumber) % 10;\n    let translateY = offset * height;\n    if (offset > 5) translateY -= 10 * height;\n    return translateY;\n  });\n\n  if (!height) {\n    return (\n      <span style={{ visibility: 'hidden', position: 'absolute' }}>\n        {number}\n      </span>\n    );\n  }\n\n  return (\n    <motion.span\n      data-slot=\"sliding-number-display\"\n      style={{\n        y,\n        position: 'absolute',\n        inset: 0,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n      transition={{ ...transition, type: 'spring' }}\n    >\n      {number}\n    </motion.span>\n  );\n}\n\ntype SlidingNumberProps = Omit<HTMLMotionProps<'span'>, 'children'> & {\n  number: number;\n  fromNumber?: number;\n  onNumberChange?: (number: number) => void;\n  padStart?: boolean;\n  decimalSeparator?: string;\n  decimalPlaces?: number;\n  thousandSeparator?: string;\n  transition?: SpringOptions;\n  delay?: number;\n  initiallyStable?: boolean;\n} & UseIsInViewOptions;\n\nfunction SlidingNumber({\n  ref,\n  number,\n  fromNumber,\n  onNumberChange,\n  inView = false,\n  inViewMargin = '0px',\n  inViewOnce = true,\n  padStart = false,\n  decimalSeparator = '.',\n  decimalPlaces = 0,\n  thousandSeparator,\n  transition = { stiffness: 200, damping: 20, mass: 0.4 },\n  delay = 0,\n  initiallyStable = false,\n  ...props\n}: SlidingNumberProps) {\n  const { ref: localRef, isInView } = useIsInView(\n    ref as React.Ref<HTMLElement>,\n    {\n      inView,\n      inViewOnce,\n      inViewMargin,\n    },\n  );\n\n  const initialNumeric = Math.abs(Number(number));\n  const prevNumberRef = React.useRef<number>(\n    initiallyStable ? initialNumeric : 0,\n  );\n\n  const hasAnimated = fromNumber !== undefined;\n\n  const motionVal = useMotionValue(\n    initiallyStable ? initialNumeric : (fromNumber ?? 0),\n  );\n  const springVal = useSpring(motionVal, { stiffness: 90, damping: 50 });\n\n  const skippedInitialWhenStable = React.useRef(false);\n\n  React.useEffect(() => {\n    if (!hasAnimated) return;\n    if (initiallyStable && !skippedInitialWhenStable.current) {\n      skippedInitialWhenStable.current = true;\n      return;\n    }\n    const timeoutId = setTimeout(() => {\n      if (isInView) motionVal.set(number);\n    }, delay);\n    return () => clearTimeout(timeoutId);\n  }, [hasAnimated, initiallyStable, isInView, number, motionVal, delay]);\n\n  const [effectiveNumber, setEffectiveNumber] = React.useState<number>(\n    initiallyStable ? initialNumeric : 0,\n  );\n\n  React.useEffect(() => {\n    if (hasAnimated) {\n      const inferredDecimals =\n        typeof decimalPlaces === 'number' && decimalPlaces >= 0\n          ? decimalPlaces\n          : (() => {\n              const s = String(number);\n              const idx = s.indexOf('.');\n              return idx >= 0 ? s.length - idx - 1 : 0;\n            })();\n\n      const factor = Math.pow(10, inferredDecimals);\n\n      const unsubscribe = springVal.on('change', (latest: number) => {\n        const newValue =\n          inferredDecimals > 0\n            ? Math.round(latest * factor) / factor\n            : Math.round(latest);\n\n        if (effectiveNumber !== newValue) {\n          setEffectiveNumber(newValue);\n          onNumberChange?.(newValue);\n        }\n      });\n      return () => unsubscribe();\n    } else {\n      setEffectiveNumber(\n        initiallyStable ? initialNumeric : !isInView ? 0 : initialNumeric,\n      );\n    }\n  }, [\n    hasAnimated,\n    springVal,\n    isInView,\n    number,\n    decimalPlaces,\n    onNumberChange,\n    effectiveNumber,\n    initiallyStable,\n    initialNumeric,\n  ]);\n\n  const formatNumber = React.useCallback(\n    (num: number) =>\n      decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(),\n    [decimalPlaces],\n  );\n\n  const numberStr = formatNumber(effectiveNumber);\n  const [newIntStrRaw, newDecStrRaw = ''] = numberStr.split('.');\n\n  const finalIntLength = padStart\n    ? Math.max(\n        Math.floor(Math.abs(number)).toString().length,\n        newIntStrRaw.length,\n      )\n    : newIntStrRaw.length;\n\n  const newIntStr = padStart\n    ? newIntStrRaw.padStart(finalIntLength, '0')\n    : newIntStrRaw;\n\n  const prevFormatted = formatNumber(prevNumberRef.current);\n  const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.');\n  const prevIntStr = padStart\n    ? prevIntStrRaw.padStart(finalIntLength, '0')\n    : prevIntStrRaw;\n\n  const adjustedPrevInt = React.useMemo(() => {\n    return prevIntStr.length > finalIntLength\n      ? prevIntStr.slice(-finalIntLength)\n      : prevIntStr.padStart(finalIntLength, '0');\n  }, [prevIntStr, finalIntLength]);\n\n  const adjustedPrevDec = React.useMemo(() => {\n    if (!newDecStrRaw) return '';\n    return prevDecStrRaw.length > newDecStrRaw.length\n      ? prevDecStrRaw.slice(0, newDecStrRaw.length)\n      : prevDecStrRaw.padEnd(newDecStrRaw.length, '0');\n  }, [prevDecStrRaw, newDecStrRaw]);\n\n  React.useEffect(() => {\n    if (isInView || initiallyStable) {\n      prevNumberRef.current = effectiveNumber;\n    }\n  }, [effectiveNumber, isInView, initiallyStable]);\n\n  const intPlaces = React.useMemo(\n    () =>\n      Array.from({ length: finalIntLength }, (_, i) =>\n        Math.pow(10, finalIntLength - i - 1),\n      ),\n    [finalIntLength],\n  );\n  const decPlaces = React.useMemo(\n    () =>\n      newDecStrRaw\n        ? Array.from({ length: newDecStrRaw.length }, (_, i) =>\n            Math.pow(10, newDecStrRaw.length - i - 1),\n          )\n        : [],\n    [newDecStrRaw],\n  );\n\n  const newDecValue = newDecStrRaw ? parseInt(newDecStrRaw, 10) : 0;\n  const prevDecValue = adjustedPrevDec ? parseInt(adjustedPrevDec, 10) : 0;\n\n  return (\n    <motion.span\n      ref={localRef}\n      data-slot=\"sliding-number\"\n      style={{\n        display: 'inline-flex',\n        alignItems: 'center',\n      }}\n      {...props}\n    >\n      {isInView && Number(number) < 0 && (\n        <span style={{ marginRight: '0.25rem' }}>-</span>\n      )}\n\n      {intPlaces.map((place, idx) => {\n        const digitsToRight = intPlaces.length - idx - 1;\n        const isSeparatorPosition =\n          typeof thousandSeparator !== 'undefined' &&\n          digitsToRight > 0 &&\n          digitsToRight % 3 === 0;\n\n        return (\n          <React.Fragment key={`int-${place}`}>\n            <SlidingNumberRoller\n              prevValue={parseInt(adjustedPrevInt, 10)}\n              value={parseInt(newIntStr ?? '0', 10)}\n              place={place}\n              transition={transition}\n            />\n            {isSeparatorPosition && <span>{thousandSeparator}</span>}\n          </React.Fragment>\n        );\n      })}\n\n      {newDecStrRaw && (\n        <>\n          <span>{decimalSeparator}</span>\n          {decPlaces.map((place) => (\n            <SlidingNumberRoller\n              key={`dec-${place}`}\n              prevValue={prevDecValue}\n              value={newDecValue}\n              place={place}\n              transition={transition}\n              delay={delay}\n            />\n          ))}\n        </>\n      )}\n    </motion.span>\n  );\n}\n\nexport { SlidingNumber, type SlidingNumberProps };\n",
      "type": "registry:ui",
      "target": "components/odysseyui/primitives/texts/sliding-number.tsx"
    }
  ],
  "type": "registry:ui"
}