"use client" import { easeOut, LayoutGroup, motion, MotionConfig, MotionConfigContext, type HTMLMotionProps, type MotionConfigProps, } from "motion/react" import { ComponentProps, forwardRef, useContext, useMemo } from "react" import { formatToParts } from "../utils/format-parts" import { Mask, maskHeight } from "./Mask" import { NumberSection } from "./NumberSection" export const DEFAULT_TRANSITION = { // We use keyframes and times so the opacity/exit animations can last // as long as the layout animation, so Framer Motion doesn't have to // remove exiting elements until the layout animation is done. // This worked better in testing than safeToRemove() from usePresence() opacity: { duration: 1, ease: easeOut }, // perceptual duration of 0.5s layout: { type: "spring", duration: 1, bounce: 0 }, y: { type: "spring", duration: 1, bounce: 0 }, } as const satisfies MotionConfigProps["transition"] export type AnimateNumberProps = Omit, "children"> & { children: number | bigint | string locales?: Intl.LocalesArgument // Scientific and engineering notation are not supported atm: format?: Omit & { notation?: Exclude< Intl.NumberFormatOptions["notation"], "scientific" | "engineering" > } transition?: ComponentProps["transition"] suffix?: string prefix?: string } export const AnimateNumber = forwardRef( function AnimateNumber( { children: value, locales, format, transition, style, suffix, prefix, ...rest }, ref ) { // Split the number into parts const parts = useMemo( () => formatToParts(value, { locales, format }, prefix, suffix), [value, locales, format] ) const { pre, integer, fraction, post, formatted } = parts const contextTransition = useContext(MotionConfigContext).transition transition = transition ?? contextTransition ?? DEFAULT_TRANSITION return ( {/* These last two sections need to have layout animations so that if new characters are added in-flight they know where to go: */} ) } )