{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-texts-scrolling-number",
  "title": "Scrolling Number",
  "description": "A scrolling number animation.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@unlumen-ui/hooks-use-is-in-view",
    "@unlumen-ui/lib-get-strict-context"
  ],
  "files": [
    {
      "path": "registry/primitives/texts/scrolling-number/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type MotionValue,\n  type HTMLMotionProps,\n} from \"motion/react\";\n\nimport {\n  useIsInView,\n  type UseIsInViewOptions,\n} from \"@/hooks/use-is-in-view\";\nimport { getStrictContext } from \"@/lib/get-strict-context\";\n\nconst formatter = new Intl.NumberFormat(\"en-US\");\n\nfunction generateRange(\n  max: number,\n  step: number,\n  sideItemsCount: number,\n): number[] {\n  const result: number[] = [];\n  const end = max + sideItemsCount * step;\n  for (let value = end; value >= 0; value -= step) {\n    result.push(value);\n  }\n  return result;\n}\n\ntype ScrollingNumberDirection = \"ltr\" | \"rtl\" | \"ttb\" | \"btt\";\n\ntype ScrollingNumberContextType = {\n  number: number;\n  step: number;\n  itemsSize: number;\n  sideItemsCount: number;\n  displayedItemsCount: number;\n  isInView: boolean;\n  direction: ScrollingNumberDirection;\n  isVertical: boolean;\n  range: number[];\n  onNumberChange?: (value: number) => void;\n};\n\nconst [ScrollingNumberProvider, useScrollingNumber] =\n  getStrictContext<ScrollingNumberContextType>(\"ScrollingNumberContext\");\n\ntype ScrollingNumberContainerProps = React.ComponentProps<\"div\"> & {\n  number: number;\n  step: number;\n  itemsSize?: number;\n  sideItemsCount?: number;\n  direction?: ScrollingNumberDirection;\n  onNumberChange?: (value: number) => void;\n} & UseIsInViewOptions;\n\nfunction ScrollingNumberContainer({\n  ref,\n  number,\n  step,\n  itemsSize = 30,\n  sideItemsCount = 2,\n  direction = \"btt\",\n  inView = false,\n  inViewMargin = \"0px\",\n  inViewOnce = true,\n  onNumberChange,\n  style,\n  ...props\n}: ScrollingNumberContainerProps) {\n  const { ref: localRef, isInView } = useIsInView(\n    ref as React.Ref<HTMLDivElement>,\n    {\n      inView,\n      inViewOnce,\n      inViewMargin,\n    },\n  );\n\n  const displayedItemsCount = React.useMemo(\n    () => 1 + sideItemsCount * 2,\n    [sideItemsCount],\n  );\n  const isVertical = React.useMemo(\n    () => direction === \"btt\" || direction === \"ttb\",\n    [direction],\n  );\n  const range = React.useMemo(\n    () => generateRange(number, step, sideItemsCount),\n    [number, step, sideItemsCount],\n  );\n\n  return (\n    <ScrollingNumberProvider\n      value={{\n        number,\n        step,\n        itemsSize,\n        sideItemsCount,\n        displayedItemsCount,\n        isInView,\n        direction,\n        isVertical,\n        range,\n        onNumberChange,\n      }}\n    >\n      <div\n        ref={localRef}\n        data-slot=\"scrolling-number-container\"\n        data-direction={direction}\n        style={{\n          position: \"relative\",\n          overflow: \"hidden\",\n          height: isVertical ? itemsSize * displayedItemsCount : undefined,\n          width: !isVertical ? itemsSize * displayedItemsCount : undefined,\n          ...style,\n        }}\n        {...props}\n      />\n    </ScrollingNumberProvider>\n  );\n}\n\ntype ScrollingNumberHighlightProps = React.ComponentProps<\"div\">;\n\nfunction ScrollingNumberHighlight({\n  style,\n  ...props\n}: ScrollingNumberHighlightProps) {\n  const { itemsSize, isVertical, direction } = useScrollingNumber();\n  return (\n    <div\n      data-slot=\"scrolling-number-highlight\"\n      data-direction={direction}\n      style={{\n        position: \"absolute\",\n        height: isVertical ? itemsSize : undefined,\n        width: !isVertical ? itemsSize : undefined,\n        left: !isVertical ? \"50%\" : undefined,\n        top: isVertical ? \"50%\" : undefined,\n        transform: !isVertical ? \"translateX(-50%)\" : \"translateY(-50%)\",\n        zIndex: 0,\n        ...style,\n      }}\n      {...props}\n    />\n  );\n}\n\ntype ScrollingNumberProps = HTMLMotionProps<\"div\"> & {\n  delay?: number;\n  onCompleted?: () => void;\n};\n\nfunction ScrollingNumber({\n  transition = { stiffness: 90, damping: 30 },\n  delay = 0,\n  onCompleted,\n  style,\n  ...props\n}: ScrollingNumberProps) {\n  const {\n    itemsSize,\n    sideItemsCount,\n    displayedItemsCount,\n    isInView,\n    direction,\n    isVertical,\n    range,\n    step,\n    number,\n    onNumberChange,\n  } = useScrollingNumber();\n\n  const motionKey: \"x\" | \"y\" = isVertical ? \"y\" : \"x\";\n  const initialOffset = itemsSize * sideItemsCount;\n  const travel = itemsSize * (range.length - displayedItemsCount);\n\n  let initialPosition: number;\n  let finalPosition: number;\n\n  switch (direction) {\n    case \"btt\":\n      initialPosition = -initialOffset;\n      finalPosition = travel;\n      break;\n    case \"ttb\":\n      initialPosition = initialOffset;\n      finalPosition = -travel;\n      break;\n    case \"rtl\":\n      initialPosition = -initialOffset;\n      finalPosition = travel;\n      break;\n    case \"ltr\":\n      initialPosition = initialOffset;\n      finalPosition = -travel;\n      break;\n    default:\n      initialPosition = -initialOffset;\n      finalPosition = travel;\n  }\n\n  const posMotion: MotionValue<number> = useMotionValue(initialPosition);\n  const posSpring = useSpring(posMotion, transition);\n\n  React.useEffect(() => {\n    if (!isInView) return;\n    const timer = setTimeout(() => {\n      posMotion.set(finalPosition);\n    }, delay);\n    return () => clearTimeout(timer);\n  }, [isInView, finalPosition, posMotion, delay]);\n\n  const currentIndex = useTransform(\n    posSpring,\n    (p) => Math.abs(p) / itemsSize + sideItemsCount,\n  );\n  const currentValue = useTransform(currentIndex, (idx) => idx * step);\n  const snappedValue = useTransform(\n    currentIndex,\n    (idx) => Math.round(idx) * step,\n  );\n\n  const completedTransform = useTransform(\n    currentValue,\n    (val) => val >= number * 0.99,\n  );\n\n  React.useEffect(() => {\n    const unsubscribe = completedTransform.on(\"change\", (latest) => {\n      if (latest) onCompleted?.();\n    });\n    return unsubscribe;\n  }, [completedTransform, onCompleted]);\n\n  React.useEffect(() => {\n    const unsub = snappedValue.on(\"change\", (val) => {\n      const bounded = val < 0 ? 0 : val > number ? number : val;\n      onNumberChange?.(bounded);\n    });\n    return unsub;\n  }, [snappedValue, onNumberChange, number]);\n\n  const directionMap: Record<\n    ScrollingNumberDirection,\n    React.CSSProperties[\"flexDirection\"]\n  > = {\n    btt: \"column\",\n    ttb: \"column-reverse\",\n    rtl: \"row\",\n    ltr: \"row-reverse\",\n  };\n\n  return (\n    <motion.div\n      data-slot=\"scrolling-number\"\n      style={{\n        position: \"absolute\",\n        top: direction === \"ttb\" ? 0 : undefined,\n        bottom: direction === \"btt\" ? 0 : undefined,\n        left: direction === \"ltr\" ? 0 : undefined,\n        right: direction === \"rtl\" ? 0 : undefined,\n        width: isVertical ? \"100%\" : undefined,\n        height: !isVertical ? \"100%\" : undefined,\n        display: \"flex\",\n        zIndex: 1,\n        flexDirection: directionMap[direction],\n        [motionKey]: posSpring,\n        ...style,\n      }}\n      {...props}\n    />\n  );\n}\n\ntype ScrollingNumberItemsProps = Omit<React.ComponentProps<\"div\">, \"children\">;\n\nfunction ScrollingNumberItems({ style, ...props }: ScrollingNumberItemsProps) {\n  const { range, direction, itemsSize, isVertical } = useScrollingNumber();\n  return range.map((value) => (\n    <div\n      key={value}\n      data-slot=\"scrolling-number-item\"\n      data-value={value}\n      data-direction={direction}\n      style={{\n        height: isVertical ? itemsSize : undefined,\n        width: !isVertical ? itemsSize : undefined,\n        ...style,\n      }}\n      {...props}\n    >\n      {formatter.format(value)}\n    </div>\n  ));\n}\n\nexport {\n  ScrollingNumberContainer,\n  ScrollingNumber,\n  ScrollingNumberHighlight,\n  ScrollingNumberItems,\n  useScrollingNumber,\n  type ScrollingNumberContainerProps,\n  type ScrollingNumberProps,\n  type ScrollingNumberHighlightProps,\n  type ScrollingNumberItemsProps,\n  type ScrollingNumberDirection,\n  type ScrollingNumberContextType,\n};\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/primitives/texts/scrolling-number.tsx"
    }
  ],
  "type": "registry:ui"
}