{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "odometer",
  "type": "registry:ui",
  "title": "Odometer",
  "description": "A number that rolls each digit on its own wheel, up when the value grows and down when it shrinks, and slides new digits in as the number gets longer.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/odometer.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  PresenceContext,\n  useInView,\n  useMotionValue,\n  usePresence,\n  usePresenceData,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * ODOMETER STORYBOARD\n *\n *  rest      plain text: every digit sits crisp in its own\n *            window, on the baseline of the words around it\n *  change    each digit that differs rolls on its wheel of\n *            0–9: up when the value grows, down when it\n *            shrinks, the long way round if it has to (19 → 21\n *            rolls the ones through 0). Digits that didn't\n *            change don't move at all\n *  ripple    the rightmost changing digit goes first and each\n *            place to its left follows 30ms later, like a carry\n *  land      a spring brings each wheel home in about 0.6s,\n *            a hair past its digit and back. Long rolls are\n *            damped a little more, so nothing ever bounces\n *  grow      a new leading digit, and its comma, roll in from\n *            below as their width eases open; a digit that's\n *            no longer needed rolls out and its width closes\n *  swap      symbols ($ , . % K −) don't roll. They ride along\n *            with the layout and crossfade when they change\n *  redirect  a new value mid-roll turns every wheel from where\n *            it is, at the speed it's going\n *  reveal    with rollOnView, the wheels wait on 0 until the\n *            number scrolls into view, then roll up to it\n *\n *  Each wheel's position is one continuous number (the ones\n *  wheel goes 9 → 11 for 19 → 21) and its digits are drawn\n *  by modulo, so wraps and retargets need no special cases.\n *  Rolls are transforms on motion values: no React render\n *  per frame.\n * ───────────────────────────────────────────────────────── */\n\nconst ODOMETER = {\n  pitch: 1.05, // em between neighboring digits on a wheel; a digit arrives or leaves by one\n  travel: 1.35, // em a symbol rolls to arrive or leave\n  overshoot: 0.045, // furthest a wheel may run past its digit, in digits\n  drift: 0.35, // em a changing symbol slides as it crossfades\n  fade: 0.15, // s; reduced motion crossfades digits instead of rolling them\n  reveal: 0.6, // share of the number in view before rollOnView rolls\n};\n\n// The window a glyph shows through: 0.35em taller than the line above and\n// below, with negative margins so the line's height and width don't change.\nconst WINDOW =\n  \"pointer-events-none relative block shrink-0 -mx-[0.25em] -my-[0.35em] px-[0.25em] py-[0.35em]\";\n// Digits are opaque within 0.4em of the line's middle, just room for the\n// glyph and its overshoot, and gone by 0.62em: a digit fades the moment it\n// leaves its place, like passing the edge of a window. A resting neighbor, a\n// pitch away, sits wholly in the clear.\nconst DIGIT_MASK =\n  \"[mask-image:linear-gradient(to_bottom,transparent_calc(50%-0.62em),#000_calc(50%-0.4em),#000_calc(50%+0.4em),transparent_calc(50%+0.62em))]\";\n// Symbols keep a little more room, for $ and the tails of commas.\nconst SYMBOL_MASK =\n  \"[mask-image:linear-gradient(to_bottom,transparent_calc(50%-0.72em),#000_calc(50%-0.5em),#000_calc(50%+0.58em),transparent_calc(50%+0.8em))]\";\n// One glyph, laid out exactly like the sizer inside the window.\nconst CELL = \"absolute inset-x-0 top-0 h-full px-[0.25em] py-[0.35em]\";\nconst ALIGN = { start: \"justify-start\", center: \"justify-center\", end: \"justify-end\" };\nconst TEXT = { start: \"text-left\", center: \"text-center\", end: \"text-right\" };\n// A wheel: 9, 0, 1 … 9, 0, so the window always has a neighbor on both sides.\nconst CELLS = Array.from({ length: 12 }, (_, i) => i);\n\ntype Align = keyof typeof ALIGN;\n\ntype Part = {\n  /** Stable across values: digits and commas are keyed by place, so a new leading digit doesn't re-key the rest. */\n  key: string;\n  text: string;\n  /** 0–9 for a digit, null for a symbol. */\n  digit: number | null;\n  /** Place value of a digit or of the digit left of a comma: 0 is ones, -1 tenths. */\n  place: number | null;\n  /** The side the slot hangs from while it's narrower than its glyph. */\n  align: Align;\n};\n\ntype Change = {\n  sig: string;\n  value: number;\n  parts: Part[];\n  pending: boolean;\n  /** 1 when the value grew, -1 when it shrank. */\n  dir: number;\n  /** Place of the rightmost digit that changed: the ripple starts there. */\n  origin: number;\n  /** Jump instead of rolling. */\n  instant: boolean;\n  /** The first roll up from 0 with rollOnView. */\n  reveal: boolean;\n};\n\ntype Config = {\n  stiffness: number;\n  damping: number;\n  stagger: number;\n  delay: number;\n  reduced: boolean;\n};\n\ntype Grouping = \"international\" | \"indian\";\n\ntype Formatter = {\n  nf: Intl.NumberFormat;\n  digits: string[];\n  /** The locale's thousands separator. */\n  separator: string;\n  /** Fewest integer digits that get a separator: 4, 5 for \"min2\", never when grouping is off. */\n  least: number;\n};\n\nconst formatters = new Map<string, Formatter>();\n\nfunction getFormatter(locale: string, format: Intl.NumberFormatOptions | undefined): Formatter {\n  const id = `${locale} ${JSON.stringify(format ?? {})}`;\n  let f = formatters.get(id);\n  if (!f) {\n    const nf = new Intl.NumberFormat(locale, format);\n    // The locale's own digits, so wheels in other numbering systems still roll.\n    const plain = new Intl.NumberFormat(locale, {\n      numberingSystem: nf.resolvedOptions().numberingSystem,\n      useGrouping: false,\n    });\n    const useGrouping = nf.resolvedOptions().useGrouping as unknown;\n    f = {\n      nf,\n      digits: CELLS.slice(0, 10).map((d) => plain.format(d)),\n      separator:\n        [nf, new Intl.NumberFormat(locale)]\n          .map((n) => n.formatToParts(1e6).find((p) => p.type === \"group\")?.value)\n          .find(Boolean) ?? \",\",\n      least: useGrouping === false ? Infinity : useGrouping === \"min2\" ? 5 : 4,\n    };\n    if (formatters.size > 200) formatters.clear();\n    formatters.set(id, f);\n  }\n  return f;\n}\n\n/**\n * Moves the separators to a grouping of its own: threes all the way\n * (1,234,567), or the Indian lakh and crore, three then twos (12,34,567).\n * Everything else the locale chose, symbols and separator alike, stays.\n */\nfunction regroup(\n  raw: Intl.NumberFormatPart[],\n  grouping: Grouping | undefined,\n  { separator, least }: Pick<Formatter, \"separator\" | \"least\">,\n) {\n  if (!grouping || least === Infinity) return raw;\n  const integer = Array.from(\n    raw\n      .filter((p) => p.type === \"integer\")\n      .map((p) => p.value)\n      .join(\"\"),\n  );\n  const out: Intl.NumberFormatPart[] = [];\n  let placed = false;\n  for (const p of raw) {\n    if (p.type === \"group\") continue;\n    if (p.type !== \"integer\") {\n      out.push(p);\n      continue;\n    }\n    if (placed) continue;\n    placed = true;\n    const groups: string[] = [];\n    let end = integer.length;\n    let size = integer.length < least ? end : 3;\n    while (end > 0) {\n      groups.unshift(integer.slice(Math.max(0, end - size), end).join(\"\"));\n      end -= size;\n      if (grouping === \"indian\") size = 2;\n    }\n    groups.forEach((value, i) => {\n      if (i > 0) out.push({ type: \"group\", value: separator });\n      out.push({ type: \"integer\", value });\n    });\n  }\n  return out;\n}\n\nfunction toParts(raw: Intl.NumberFormatPart[], digits: string[]): Part[] {\n  let place = 0;\n  for (const p of raw) if (p.type === \"integer\") place += Array.from(p.value).length;\n\n  const parts: Part[] = [];\n  const seen: Record<string, number> = {};\n  let fraction = 0;\n  let inside = false; // past the first digit\n  for (const p of raw) {\n    if (p.type === \"integer\" || p.type === \"fraction\") {\n      inside = true;\n      for (const char of Array.from(p.value)) {\n        const at = p.type === \"integer\" ? --place : -++fraction;\n        const digit = digits.indexOf(char);\n        parts.push({\n          key: `d${at}`,\n          text: char,\n          digit: digit < 0 ? null : digit,\n          place: at,\n          align: at < 0 ? \"start\" : \"end\",\n        });\n      }\n    } else if (p.type === \"group\") {\n      parts.push({ key: `g${place}`, text: p.value, digit: null, place, align: \"end\" });\n    } else if (p.type === \"decimal\") {\n      inside = true;\n      parts.push({ key: \"decimal\", text: p.value, digit: null, place: null, align: \"center\" });\n    } else {\n      // + and − share a slot, so a sign flip crossfades; so do K, M and B.\n      const type = p.type === \"plusSign\" || p.type === \"minusSign\" ? \"sign\" : p.type;\n      const id = `${inside ? \"post\" : \"pre\"}-${type}`;\n      seen[id] = (seen[id] ?? -1) + 1;\n      parts.push({\n        key: `${id}-${seen[id]}`,\n        // A true minus, the same width as the plus it swaps with.\n        text: p.type === \"minusSign\" ? p.value.replace(\"-\", \"−\") : p.value,\n        digit: null,\n        place: null,\n        align: inside ? \"start\" : \"end\",\n      });\n    }\n  }\n  return parts;\n}\n\n/** The rightmost place whose digit differs, counting added and removed digits. */\nfunction originOf(before: Part[], after: Part[]) {\n  const was = new Map(before.map((p) => [p.key, p]));\n  const is = new Map(after.map((p) => [p.key, p]));\n  let origin = Infinity;\n  for (const p of after) {\n    if (p.place !== null && was.get(p.key)?.text !== p.text) origin = Math.min(origin, p.place);\n  }\n  for (const p of before) {\n    if (p.place !== null && !is.has(p.key)) origin = Math.min(origin, p.place);\n  }\n  return Number.isFinite(origin) ? origin : 0;\n}\n\nconst mod10 = (n: number) => ((n % 10) + 10) % 10;\n\n/**\n * The roll's spring. Overshoot grows with the distance rolled, so long rolls\n * (and fast retargets) get just enough extra damping to stay under\n * ODOMETER.overshoot: a hint past the digit, never a bounce.\n */\nfunction rollSpring(cfg: Config, distance: number, velocity: number) {\n  const omega = Math.sqrt(cfg.stiffness);\n  const zeta = cfg.damping / (2 * omega);\n  const reach = Math.abs(distance) + Math.max(0, Math.sign(distance) * velocity) / omega;\n  let damping = cfg.damping;\n  if (zeta < 1 && reach > ODOMETER.overshoot) {\n    const r = Math.min(Math.exp((-Math.PI * zeta) / Math.sqrt(1 - zeta * zeta)), ODOMETER.overshoot / reach);\n    const ln = Math.log(r);\n    damping = Math.max(damping, (-2 * omega * ln) / Math.sqrt(Math.PI * Math.PI + ln * ln));\n  }\n  return { type: \"spring\" as const, stiffness: cfg.stiffness, damping, restDelta: 0.001, restSpeed: 0.01 };\n}\n\n/** Width opens and closes on the same spring, critically damped: layout never wobbles. */\nfunction widthSpring(cfg: Config) {\n  return {\n    type: \"spring\" as const,\n    stiffness: cfg.stiffness,\n    damping: 2 * Math.sqrt(cfg.stiffness),\n    restDelta: 0.1,\n    restSpeed: 1,\n  };\n}\n\nconst subscribe = () => () => {};\nconst offscreen: React.RefObject<Element | null> = { current: null };\n\nexport interface OdometerProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n  /** The number to show. Changes roll. */\n  value: number;\n  /** Intl.NumberFormat options: currency, decimals, percent, compact, units. */\n  format?: Intl.NumberFormatOptions;\n  /** Locale for formatting. Fixed by default so the server and browser agree. */\n  locale?: string;\n  /** Spring stiffness of the roll. Higher is snappier. */\n  stiffness?: number;\n  /** Spring damping of the roll. Lower overshoots more; long rolls get a little extra. */\n  damping?: number;\n  /** Seconds between neighboring places starting to roll, from right to left. */\n  stagger?: number;\n  /** Seconds to wait before rolling, after a change or on scrolling into view. */\n  delay?: number;\n  /** Hold every wheel on 0 until the number first scrolls into view, then roll up to it. */\n  rollOnView?: boolean;\n  /** Where the separators go: by thousands, or by lakh and crore. Follows the locale when unset. */\n  grouping?: Grouping;\n}\n\n/**\n * A number that rolls each digit on its own wheel. Inline: it inherits the\n * font, size and color, and sits on the baseline of the text around it.\n */\nexport function Odometer({\n  value,\n  format,\n  locale = \"en-US\",\n  stiffness = 150,\n  damping = 18,\n  stagger = 0.03,\n  delay = 0,\n  rollOnView = false,\n  grouping,\n  className,\n  ...props\n}: OdometerProps) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  // False while rendering on the server and hydrating that HTML, so both draw\n  // the final value; true from the first render after.\n  const hydrated = React.useSyncExternalStore(subscribe, () => true, () => false);\n  const reduced = (useReducedMotion() ?? false) && hydrated;\n  const seen = useInView(rollOnView ? ref : offscreen, { once: true, amount: ODOMETER.reveal });\n  const pending = rollOnView && hydrated && !seen && !reduced;\n\n  const { nf, digits, separator, least } = getFormatter(locale, format);\n  const raw = regroup(nf.formatToParts(value), grouping, { separator, least });\n  const final = toParts(raw, digits);\n  const parts = pending\n    ? final.map((p) => (p.digit === null ? p : { ...p, digit: 0, text: digits[0] }))\n    : final;\n  const shown = pending ? 0 : value;\n  const sig = parts.map((p) => p.key + p.text).join(\" \");\n\n  const [change, setChange] = React.useState<Change>(() => ({\n    sig,\n    value: shown,\n    parts,\n    pending,\n    dir: 1,\n    origin: 0,\n    instant: true,\n    reveal: false,\n  }));\n  if (change.sig !== sig || change.pending !== pending) {\n    const reveal = change.pending && !pending;\n    setChange({\n      sig,\n      value: shown,\n      parts,\n      pending,\n      dir: reveal || shown >= change.value ? 1 : -1,\n      origin: originOf(change.parts, parts),\n      // Anything that happens before the reveal happens off screen.\n      instant: pending,\n      reveal,\n    });\n  }\n\n  const cfg = React.useMemo<Config>(\n    () => ({ stiffness, damping, stagger, delay, reduced }),\n    [stiffness, damping, stagger, delay, reduced],\n  );\n\n  return (\n    <span\n      ref={ref}\n      className={cn(\"relative inline-flex items-baseline whitespace-pre tabular-nums lining-nums\", className)}\n      {...props}\n    >\n      <span className=\"sr-only\">{raw.map((p) => p.value).join(\"\")}</span>\n      <AnimatePresence initial={false} custom={change}>\n        {change.parts.map((part) => (\n          <Slot\n            key={part.key}\n            part={part}\n            change={change}\n            cfg={cfg}\n            digits={digits}\n            full={rollOnView && hydrated}\n          />\n        ))}\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/**\n * One digit or symbol. Owns its presence: a slot that arrives rolls in as its\n * width opens, and one that leaves rolls out as its width closes.\n */\nfunction Slot({\n  part,\n  change,\n  cfg,\n  digits,\n  full,\n}: {\n  part: Part;\n  change: Change;\n  cfg: Config;\n  /** The locale's 0–9. */\n  digits: string[];\n  full: boolean;\n}) {\n  const context = React.useContext(PresenceContext);\n  const [isPresent, safeToRemove] = usePresence();\n  // A leaving slot keeps its last props; the change that removed it comes from here.\n  const removedBy = usePresenceData() as Change | undefined;\n  const now = isPresent ? change : (removedBy ?? change);\n  const wait = cfg.delay + (part.place === null ? 0 : Math.max(0, part.place - now.origin) * cfg.stagger);\n\n  // Slots in the first render are just there; later ones arrive.\n  const [arrives] = React.useState(\n    () => context !== null && context.initial !== false && !change.instant && !cfg.reduced,\n  );\n  const width = useMotionValue<number | \"auto\">(arrives ? 0 : \"auto\");\n  const shift = useMotionValue(arrives ? change.dir : 0); // in travels; +1 is one below\n\n  const slotRef = React.useRef<HTMLSpanElement>(null);\n  const sizerRef = React.useRef<HTMLSpanElement>(null);\n  const latest = React.useRef({ wait, dir: now.dir, instant: now.instant, cfg, safeToRemove });\n  React.useLayoutEffect(() => {\n    latest.current = { wait, dir: now.dir, instant: now.instant, cfg, safeToRemove };\n  });\n\n  // Arrive, leave, or come back while leaving.\n  React.useLayoutEffect(() => {\n    const slot = slotRef.current;\n    const sizer = sizerRef.current;\n    if (!slot || !sizer) return;\n    const { wait, dir, instant, cfg, safeToRemove } = latest.current;\n    const moving = Math.abs(shift.getVelocity()) > 0.01;\n    const delay = moving ? 0 : wait;\n\n    if (isPresent) {\n      if (width.get() === \"auto\" && shift.get() === 0) return;\n      if (cfg.reduced || instant) {\n        width.set(\"auto\");\n        shift.jump(0);\n        return;\n      }\n      if (width.get() === \"auto\") width.jump(slot.getBoundingClientRect().width);\n      animate(width, sizer.getBoundingClientRect().width, {\n        ...widthSpring(cfg),\n        delay,\n        onComplete: () => width.set(\"auto\"),\n      });\n      animate(shift, 0, { ...rollSpring(cfg, -shift.get(), shift.getVelocity()), delay });\n      return;\n    }\n\n    if (cfg.reduced || instant) {\n      safeToRemove?.();\n      return;\n    }\n    if (width.get() === \"auto\") width.jump(slot.getBoundingClientRect().width);\n    let left = 2;\n    const done = () => {\n      if (--left === 0) safeToRemove?.();\n    };\n    animate(width, 0, { ...widthSpring(cfg), delay, onComplete: done });\n    animate(shift, -dir, {\n      ...rollSpring(cfg, -dir - shift.get(), shift.getVelocity()),\n      delay,\n      onComplete: done,\n    });\n  }, [isPresent, width, shift]);\n\n  // A symbol that changes (\"K\" → \"M\") eases to its new width as it crossfades.\n  // Digits all share one width, so they skip this.\n  const natural = React.useRef<number | null>(null);\n  React.useLayoutEffect(() => {\n    const sizer = sizerRef.current;\n    if (part.digit !== null || !sizer) return;\n    const to = sizer.getBoundingClientRect().width;\n    const from = natural.current;\n    natural.current = to;\n    const { cfg, instant } = latest.current;\n    if (from === null || cfg.reduced || instant) return;\n    if (width.get() === \"auto\") {\n      if (Math.abs(from - to) < 0.5) return;\n      width.jump(from);\n    }\n    animate(width, to, { ...widthSpring(cfg), onComplete: () => width.set(\"auto\") });\n  }, [part.text, part.digit, width]);\n\n  return (\n    // The wheels inside answer to this slot's presence, not to the number's.\n    <PresenceContext.Provider value={null}>\n      <motion.span\n        ref={slotRef}\n        aria-hidden\n        className={cn(\"flex min-w-0 items-baseline select-none\", ALIGN[part.align])}\n        style={{ width }}\n      >\n        <span className={cn(WINDOW, part.digit !== null && !cfg.reduced ? DIGIT_MASK : SYMBOL_MASK)}>\n          <span ref={sizerRef} className=\"invisible\">\n            {part.text}\n          </span>\n          {part.digit !== null && !cfg.reduced ? (\n            <Wheel\n              digit={part.digit}\n              change={now}\n              wait={wait}\n              cfg={cfg}\n              shift={shift}\n              solo={!isPresent}\n              digits={digits}\n              full={full}\n            />\n          ) : (\n            <Glyphs text={part.text} align={part.align} dir={now.dir} cfg={cfg} shift={shift} />\n          )}\n        </span>\n      </motion.span>\n    </PresenceContext.Provider>\n  );\n}\n\n/** A strip of digits that rolls through the window. */\nfunction Wheel({\n  digit,\n  change,\n  wait,\n  cfg,\n  shift,\n  solo,\n  digits,\n  full,\n}: {\n  digit: number;\n  change: Change;\n  wait: number;\n  cfg: Config;\n  shift: MotionValue<number>;\n  /** Show only the digit itself, for rolling in and out without neighbors. */\n  solo: boolean;\n  digits: string[];\n  /** Draw the whole wheel even before it turns, so a 0 can spin all the way round. */\n  full: boolean;\n}) {\n  // A wheel that has never turned draws only its digit, so the server's HTML\n  // is just the number.\n  const [first] = React.useState(digit);\n  const [turned, setTurned] = React.useState(false);\n  if (!turned && digit !== first) setTurned(true);\n\n  const pos = useMotionValue(digit);\n  const y = useTransform(\n    [pos, shift],\n    ([p, s]: number[]) => `${(s - mod10(p) - 1) * ODOMETER.pitch}em`,\n  );\n  const goal = React.useRef(digit); // where the wheel is heading, unwrapped\n  const due = React.useRef(0); // when a staggered roll that hasn't started yet begins, ms\n  const spun = React.useRef<Change | null>(null);\n\n  const latest = React.useRef({ change, wait, cfg });\n  React.useLayoutEffect(() => {\n    latest.current = { change, wait, cfg };\n  });\n\n  React.useLayoutEffect(() => {\n    const { change, wait, cfg } = latest.current;\n    const from = mod10(goal.current);\n    let steps = change.dir > 0 ? mod10(digit - from) : -mod10(from - digit);\n    // On the reveal a 0 turns once around, so every wheel comes alive.\n    if (change.reveal && steps === 0 && spun.current !== change) steps = 10;\n    spun.current = change;\n    if (steps === 0) return;\n    goal.current += steps;\n    if (change.instant) {\n      pos.jump(goal.current);\n      return;\n    }\n    // A wheel already turning redirects at once. One still waiting for its\n    // turn in the ripple keeps its place in line, so fast changes can't hold\n    // it back forever.\n    const t = performance.now();\n    const moving = Math.abs(pos.getVelocity()) > 0.01;\n    const delay = t < due.current ? due.current - t : moving ? 0 : wait * 1000;\n    due.current = t + delay;\n    animate(pos, goal.current, {\n      ...rollSpring(cfg, goal.current - pos.get(), pos.getVelocity()),\n      delay: delay / 1000,\n    });\n  }, [digit, change, pos]);\n\n  const cells = solo || !(turned || full) ? [digit + 1] : CELLS;\n  return (\n    <motion.span className=\"absolute inset-0\" style={{ y }}>\n      {cells.map((i) => (\n        <span key={i} className={cn(CELL, \"text-center\")} style={{ top: `${i * ODOMETER.pitch}em` }}>\n          {digits[(i + 9) % 10]}\n        </span>\n      ))}\n    </motion.span>\n  );\n}\n\nconst swap = {\n  enter: (dir: number) => ({ opacity: 0, y: `${dir * ODOMETER.drift}em` }),\n  center: { opacity: 1, y: \"0em\" },\n  exit: (dir: number) => ({ opacity: 0, y: `${-dir * ODOMETER.drift}em` }),\n};\nconst fade = { enter: { opacity: 0 }, center: { opacity: 1 }, exit: { opacity: 0 } };\n\n/** A symbol, or a digit under reduced motion: crossfades when it changes. */\nfunction Glyphs({\n  text,\n  align,\n  dir,\n  cfg,\n  shift,\n}: {\n  text: string;\n  align: Align;\n  dir: number;\n  cfg: Config;\n  shift: MotionValue<number>;\n}) {\n  const y = useTransform(shift, (s) => `${s * ODOMETER.travel}em`);\n  return (\n    <motion.span className=\"absolute inset-0\" style={{ y }}>\n      <AnimatePresence initial={false} custom={dir}>\n        <motion.span\n          key={text}\n          custom={dir}\n          variants={cfg.reduced ? fade : swap}\n          initial=\"enter\"\n          animate=\"center\"\n          exit=\"exit\"\n          transition={\n            cfg.reduced\n              ? { duration: ODOMETER.fade, ease: \"linear\" }\n              : { y: rollSpring(cfg, 1, 0), opacity: { duration: 0.25, ease: \"easeOut\" } }\n          }\n          className={cn(CELL, TEXT[align])}\n        >\n          {text}\n        </motion.span>\n      </AnimatePresence>\n    </motion.span>\n  );\n}\n"
    }
  ]
}