{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rolling-number",
  "type": "registry:component",
  "title": "Rolling Number",
  "description": "A number whose digits roll to their new values.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/rolling-number.tsx",
      "type": "registry:component",
      "target": "@components/panelui/rolling-number.tsx",
      "content": "'use client';\n\n/**\n * RollingNumber — a number whose digits roll to their new values.\n *\n * ```tsx\n * <RollingNumber value={events} />\n * <RollingNumber value={balance} format={{ style: 'currency', currency: 'USD' }} />\n * ```\n *\n * Every digit is a column of ten, and the column is moved by a share of its\n * own height. Nothing is measured: one hidden `0` in flow gives the column the\n * width and the line box the font really has, and the ten digits stacked over\n * it are each exactly that tall, so `translateY(-30%)` is the 3 whatever the\n * type size turns out to be.\n *\n * That hidden `0` is also why the number sits on the text's baseline. A box\n * that clips its overflow takes its bottom edge as its baseline, which would\n * lift every digit above the words beside it. Here the clipping is done by an\n * absolutely positioned window inside the column, and the column's baseline\n * is still the `0`'s.\n *\n * The columns are keyed from the decimal point outwards. Going from 99 to 100\n * adds a hundreds column rather than re-labelling the two that were there, so\n * the nines roll to zeroes instead of being replaced by them.\n */\nimport { useMemo, type ComponentProps } from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nconst rollingNumberVariants = tv({\n  slots: {\n    // `tabular-nums` so a 1 is as wide as an 8 and the number does not change\n    // width, and shove whatever is beside it, while it rolls.\n    root: 'inline-flex items-baseline tabular-nums',\n    column: 'relative inline-block',\n    window: 'absolute inset-0 overflow-hidden',\n    strip:\n      'absolute inset-x-0 top-0 block h-[1000%] transition-transform ease-[cubic-bezier(0.2,0,0,1)] will-change-transform motion-reduce:transition-none',\n    digit: 'block h-[10%] text-center',\n  },\n});\n\nconst DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] as const;\n\nexport interface RollingNumberProps extends Omit<ComponentProps<'span'>, 'children'> {\n  /** The number to show. Each digit rolls to its new value on its own. */\n  value: number;\n  /**\n   * How the number is written: currency, percent, compact notation, how many\n   * decimals. Passed to `Intl.NumberFormat`.\n   */\n  format?: Intl.NumberFormatOptions;\n  /**\n   * The locale the number is written in. Defaults to `en-US` rather than the\n   * browser's, so a page rendered on a server writes the same thing the\n   * browser then does.\n   */\n  locales?: Intl.LocalesArgument;\n  /** How long a digit takes to roll, in milliseconds. */\n  duration?: number;\n}\n\ninterface Piece {\n  key: string;\n  /** A digit to roll to, or `null` for a character that is just written. */\n  digit: number | null;\n  text: string;\n}\n\n/** Splits a formatted number into rolling digits and the characters around them. */\nfunction piecesOf(parts: Intl.NumberFormatPart[]): Piece[] {\n  // Whole digits are counted from the decimal point, so the units column is\n  // the same column whatever is in front of it.\n  let whole = parts.reduce((count, part) => (part.type === 'integer' ? count + part.value.length : count), 0);\n  let fraction = 0;\n  const pieces: Piece[] = [];\n\n  parts.forEach((part, index) => {\n    if (part.type === 'integer' || part.type === 'fraction') {\n      for (const character of part.value) {\n        const digit = character >= '0' && character <= '9' ? Number(character) : null;\n        const key = part.type === 'integer' ? `i${(whole -= 1)}` : `f${(fraction += 1)}`;\n        pieces.push({ key, digit, text: character });\n      }\n      return;\n    }\n    // A separator belongs to the digits on its right; anything else is keyed\n    // by what it is, so a currency sign survives the number growing a digit.\n    const key = part.type === 'group' ? `g${whole}` : `${part.type}${index}`;\n    pieces.push({ key, digit: null, text: part.value });\n  });\n\n  return pieces;\n}\n\nexport function RollingNumber({\n  value,\n  format,\n  locales = 'en-US',\n  duration = 600,\n  className,\n  render,\n  ...props\n}: RollingNumberProps & RenderProp<'span'>) {\n  const slots = rollingNumberVariants();\n\n  // The options object is usually written inline, so it is new on every\n  // render. Its contents are what the formatter depends on.\n  const formatKey = JSON.stringify(format ?? null);\n  const formatter = useMemo(\n    () => new Intl.NumberFormat(locales, format),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [locales, formatKey]\n  );\n\n  const { label, pieces } = useMemo(() => {\n    const parts = formatter.formatToParts(value);\n    return { label: parts.map((part) => part.value).join(''), pieces: piecesOf(parts) };\n  }, [formatter, value]);\n\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        className: slots.root({ className }),\n        children: (\n          <>\n            {/* What a screen reader gets: the number, once, as it is written. */}\n            <span className=\"sr-only\">{label}</span>\n            {pieces.map((piece) =>\n              piece.digit === null ? (\n                <span key={piece.key} aria-hidden=\"true\">\n                  {piece.text}\n                </span>\n              ) : (\n                <span key={piece.key} aria-hidden=\"true\" className={slots.column()}>\n                  <span className=\"invisible\">0</span>\n                  <span className={slots.window()}>\n                    <span\n                      className={slots.strip()}\n                      style={{\n                        transform: `translateY(${-piece.digit * 10}%)`,\n                        transitionDuration: `${duration}ms`,\n                      }}\n                    >\n                      {DIGITS.map((digit) => (\n                        <span key={digit} className={slots.digit()}>\n                          {digit}\n                        </span>\n                      ))}\n                    </span>\n                  </span>\n                </span>\n              )\n            )}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nRollingNumber.displayName = 'RollingNumber';\n"
    }
  ]
}
