{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "sliding-number",
"type": "registry:ui",
"title": "Sliding Number",
"description": "A numeric display component that smoothly animates number changes with a sliding transition effect.",
"dependencies": [
"motion",
"react-use-measure"
],
"files": [
{
"path": "registry/text/sliding-number/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport {\n useSpring,\n useTransform,\n motion,\n useInView,\n type MotionValue,\n type SpringOptions,\n type UseInViewOptions,\n} from 'motion/react';\nimport useMeasure from 'react-use-measure';\n\nimport { cn } from '@/lib/utils';\n\ntype SlidingNumberRollerProps = {\n prevValue: number;\n value: number;\n place: number;\n transition: SpringOptions;\n};\n\nfunction SlidingNumberRoller({\n prevValue,\n value,\n place,\n transition,\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 animatedValue.set(targetNumber);\n }, [targetNumber, animatedValue]);\n\n const [measureRef, { height }] = useMeasure();\n\n return (\n \n 0\n {Array.from({ length: 10 }, (_, i) => (\n \n ))}\n \n );\n}\n\ntype SlidingNumberDisplayProps = {\n motionValue: MotionValue;\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 {number};\n }\n\n return (\n \n {number}\n \n );\n}\n\ntype SlidingNumberProps = React.ComponentProps<'span'> & {\n number: number | string;\n inView?: boolean;\n inViewMargin?: UseInViewOptions['margin'];\n inViewOnce?: boolean;\n padStart?: boolean;\n decimalSeparator?: string;\n decimalPlaces?: number;\n transition?: SpringOptions;\n};\n\nfunction SlidingNumber({\n ref,\n number,\n className,\n inView = false,\n inViewMargin = '0px',\n inViewOnce = true,\n padStart = false,\n decimalSeparator = '.',\n decimalPlaces = 0,\n transition = {\n stiffness: 200,\n damping: 20,\n mass: 0.4,\n },\n ...props\n}: SlidingNumberProps) {\n const localRef = React.useRef(null);\n React.useImperativeHandle(ref, () => localRef.current!);\n\n const inViewResult = useInView(localRef, {\n once: inViewOnce,\n margin: inViewMargin,\n });\n const isInView = !inView || inViewResult;\n\n const prevNumberRef = React.useRef(0);\n\n const effectiveNumber = React.useMemo(\n () => (!isInView ? 0 : Math.abs(Number(number))),\n [number, isInView],\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 const newIntStr =\n padStart && newIntStrRaw?.length === 1 ? '0' + newIntStrRaw : newIntStrRaw;\n\n const prevFormatted = formatNumber(prevNumberRef.current);\n const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.');\n const prevIntStr =\n padStart && prevIntStrRaw.length === 1\n ? '0' + prevIntStrRaw\n : prevIntStrRaw;\n\n const adjustedPrevInt = React.useMemo(() => {\n return prevIntStr.length > (newIntStr?.length ?? 0)\n ? prevIntStr.slice(-(newIntStr?.length ?? 0))\n : prevIntStr.padStart(newIntStr?.length ?? 0, '0');\n }, [prevIntStr, newIntStr]);\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) prevNumberRef.current = effectiveNumber;\n }, [effectiveNumber, isInView]);\n\n const intDigitCount = newIntStr?.length ?? 0;\n const intPlaces = React.useMemo(\n () =>\n Array.from({ length: intDigitCount }, (_, i) =>\n Math.pow(10, intDigitCount - i - 1),\n ),\n [intDigitCount],\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 \n {isInView && Number(number) < 0 && -}\n\n {intPlaces.map((place) => (\n \n ))}\n\n {newDecStrRaw && (\n <>\n {decimalSeparator}\n {decPlaces.map((place) => (\n \n ))}\n >\n )}\n \n );\n}\n\nexport { SlidingNumber, type SlidingNumberProps };\n",
"type": "registry:ui",
"target": "components/animate-ui/text/sliding-number.tsx"
}
]
}