"use client" import { AnimatePresence, motion, type AnimatePresenceProps, type HTMLMotionProps, } from "motion/react" import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react" import { useIsInitialRender } from "../hooks/use-is-initial-render" import { Em, Justify, KeyedNumberPart } from "../types" import { getWidthInEm } from "../utils/get-width-in-ems" import { targetWidths } from "../utils/target-widths" import { NumberDigit } from "./NumberDigit" import { NumberSymbol } from "./NumberSymbol" import { SectionContext } from "./SectionContext" export const NumberSection = forwardRef< HTMLSpanElement, Omit, "children"> & { parts: KeyedNumberPart[] justify?: Justify mode?: AnimatePresenceProps["mode"] name?: string } >(function NumberSection( { parts, justify = "left", mode, style, name, ...rest }, _ref ) { const ref = useRef(null) useImperativeHandle(_ref, () => ref.current!, []) const context = useMemo(() => ({ justify }), [justify]) const measuredRef = useRef(null) const isInitialRender = useIsInitialRender() // Keep a fixed width for the section, so that new characters get added to the end before the layout // animation starts, which makes them look like they were there already: const [width, setWidth] = useState() useEffect(() => { if (!measuredRef.current) return if (isInitialRender) { if (ref.current) ref.current.style.width = getWidthInEm(measuredRef.current) return } // Find the new width by removing exiting elements, measuring the measuredRef, and re-adding them // This better handles i.e. negative margins between elements. // We query the DOM because AnimatePresence overwrites ref props if the mode=popLayout const undos = Array.from(measuredRef.current.children).map((child) => { if (!(child instanceof HTMLElement)) return if (child.dataset.state === "exiting") { const next = child.nextSibling child.remove() return () => { // insertBefore() appends if next is null: if (measuredRef.current) { measuredRef.current.insertBefore(child, next) } } } const newWidth = targetWidths.get(child) if (!newWidth) return const oldWidth = child.style.width child.style.width = newWidth return () => { child.style.width = oldWidth } }) // Measure the resulting width: setWidth(getWidthInEm(measuredRef.current)) // Then undo immediately: for (let i = undos.length - 1; i >= 0; i--) { const undo = undos[i] if (undo) undo() } // Trigger a parent render/layout: }, [parts.map((p) => p.value).join("")]) return ( {/* zero width space to prevent the height from collapsing when there's no children: */} ​ {parts.map((part) => part.type === "integer" || part.type === "fraction" ? ( ) : ( {part.value} ) )} ) })