{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"digit-swap","type":"registry:component","title":"Number Animation Digit Swap","description":"Fixed-slot digits and mask glyphs that roll on change with controllable direction, stagger, replay, and suffix emphasis.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/digit-swap.tsx","type":"registry:component","target":"@components/motion/digit-swap.tsx","content":"\"use client\";\n// beui.dev/components/motion/number\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DigitSwapDirection = \"up\" | \"down\";\n\nexport interface DigitSwapProps {\n  /** Numeric or masked value rendered in fixed character slots. */\n  value: string | number;\n  /** Replays every glyph when the value itself contains unchanged characters. */\n  animationKey?: string | number;\n  /** Direction the next glyph enters from. */\n  direction?: DigitSwapDirection;\n  /** Per-glyph transition duration in seconds. */\n  duration?: number;\n  /** Delay in seconds between neighboring glyphs. */\n  stagger?: number;\n  /** Number of final characters that receive `suffixClassName`. */\n  suffixLength?: number;\n  className?: string;\n  glyphClassName?: string;\n  suffixClassName?: string;\n}\n\ntype GlyphMotionContext = {\n  direction: DigitSwapDirection;\n  reduceMotion: boolean;\n};\n\nconst GLYPH_VARIANTS = {\n  enter: ({ direction, reduceMotion }: GlyphMotionContext) => ({\n    opacity: 0,\n    transform: reduceMotion\n      ? \"none\"\n      : `translateY(${direction === \"up\" ? \"45%\" : \"-45%\"})`,\n  }),\n  visible: {\n    opacity: 1,\n    transform: \"translateY(0%)\",\n  },\n  exit: ({ direction, reduceMotion }: GlyphMotionContext) => ({\n    opacity: 0,\n    transform: reduceMotion\n      ? \"none\"\n      : `translateY(${direction === \"up\" ? \"-45%\" : \"45%\"})`,\n  }),\n};\n\n/** Fixed-slot digits and mask glyphs that roll when their value changes. */\nexport function DigitSwap({\n  value,\n  animationKey,\n  direction = \"up\",\n  duration = 0.18,\n  stagger = 0.006,\n  suffixLength = 0,\n  className,\n  glyphClassName,\n  suffixClassName,\n}: DigitSwapProps) {\n  const reduceMotion = useReducedMotion() ?? false;\n  const text = String(value);\n  const suffixStart = Math.max(0, text.length - Math.max(0, suffixLength));\n  const motionContext: GlyphMotionContext = { direction, reduceMotion };\n  const glyphs = Array.from(text, (character, position) => ({\n    character,\n    id: `glyph-${position}`,\n    position,\n  }));\n\n  return (\n    <span\n      data-slot=\"digit-swap\"\n      data-direction={direction}\n      className={cn(\"inline-flex items-center whitespace-nowrap\", className)}\n    >\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center\">\n        {glyphs.map(({ character, id, position }) => {\n          if (character === \" \") {\n            return <span key={id} className=\"inline-block w-[0.7ch]\" />;\n          }\n\n          const glyphKey =\n            animationKey === undefined\n              ? `${id}-${character}`\n              : `${id}-${character}-${animationKey}`;\n\n          return (\n            <span\n              key={id}\n              data-slot=\"digit-swap-glyph\"\n              className=\"relative inline-block h-[1.1em] w-[1ch] shrink-0 overflow-hidden align-bottom\"\n            >\n              <AnimatePresence initial={false} custom={motionContext}>\n                <motion.span\n                  key={glyphKey}\n                  custom={motionContext}\n                  variants={GLYPH_VARIANTS}\n                  initial=\"enter\"\n                  animate=\"visible\"\n                  exit=\"exit\"\n                  transition={{\n                    duration: reduceMotion ? Math.min(duration, 0.12) : duration,\n                    delay: reduceMotion ? 0 : position * stagger,\n                    ease: EASE_OUT,\n                  }}\n                  className={cn(\n                    \"absolute inset-0 flex items-center justify-center leading-none\",\n                    glyphClassName,\n                    position >= suffixStart ? suffixClassName : undefined,\n                  )}\n                >\n                  {character}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}