Fortura/apps/www/public/r/sliding-number.json
2025-08-20 04:12:49 -06:00

19 lines
6.7 KiB
JSON

{
"$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 <span\n ref={measureRef}\n data-slot=\"sliding-number-roller\"\n className=\"relative inline-block w-[1ch] overflow-x-visible overflow-y-clip leading-none tabular-nums\"\n >\n <span className=\"invisible\">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 <span className=\"invisible absolute\">{number}</span>;\n }\n\n return (\n <motion.span\n data-slot=\"sliding-number-display\"\n style={{ y }}\n className=\"absolute inset-0 flex items-center justify-center\"\n transition={{ ...transition, type: 'spring' }}\n >\n {number}\n </motion.span>\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<HTMLSpanElement>(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<number>(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 <span\n ref={localRef}\n data-slot=\"sliding-number\"\n className={cn('flex items-center', className)}\n {...props}\n >\n {isInView && Number(number) < 0 && <span className=\"mr-1\">-</span>}\n\n {intPlaces.map((place) => (\n <SlidingNumberRoller\n key={`int-${place}`}\n prevValue={parseInt(adjustedPrevInt, 10)}\n value={parseInt(newIntStr ?? '0', 10)}\n place={place}\n transition={transition}\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 />\n ))}\n </>\n )}\n </span>\n );\n}\n\nexport { SlidingNumber, type SlidingNumberProps };\n",
"type": "registry:ui",
"target": "components/animate-ui/text/sliding-number.tsx"
}
]
}