{
  "name": "scroller",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "files": [
    {
      "path": "ui/scroller.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronUp,\n} from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst DATA_TOP_SCROLL = \"data-top-scroll\";\nconst DATA_BOTTOM_SCROLL = \"data-bottom-scroll\";\nconst DATA_LEFT_SCROLL = \"data-left-scroll\";\nconst DATA_RIGHT_SCROLL = \"data-right-scroll\";\nconst DATA_TOP_BOTTOM_SCROLL = \"data-top-bottom-scroll\";\nconst DATA_LEFT_RIGHT_SCROLL = \"data-left-right-scroll\";\n\nconst scrollerVariants = cva(\"\", {\n  variants: {\n    orientation: {\n      vertical: [\n        \"overflow-y-auto\",\n        \"data-[top-scroll=true]:[mask-image:linear-gradient(0deg,#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n        \"data-[bottom-scroll=true]:[mask-image:linear-gradient(180deg,#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n        \"data-[top-bottom-scroll=true]:[mask-image:linear-gradient(#000,#000,transparent_0,#000_var(--scroll-shadow-size),#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n      ],\n      horizontal: [\n        \"overflow-x-auto\",\n        \"data-[left-scroll=true]:[mask-image:linear-gradient(270deg,#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n        \"data-[right-scroll=true]:[mask-image:linear-gradient(90deg,#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n        \"data-[left-right-scroll=true]:[mask-image:linear-gradient(to_right,#000,#000,transparent_0,#000_var(--scroll-shadow-size),#000_calc(100%_-_var(--scroll-shadow-size)),transparent)]\",\n      ],\n    },\n    hideScrollbar: {\n      true: \"[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden\",\n      false: \"\",\n    },\n  },\n  defaultVariants: {\n    orientation: \"vertical\",\n    hideScrollbar: false,\n  },\n});\n\ntype ScrollDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\ntype ScrollVisibility = {\n  [key in ScrollDirection]: boolean;\n};\n\ninterface ScrollerProps\n  extends VariantProps<typeof scrollerVariants>, React.ComponentProps<\"div\"> {\n  size?: number;\n  offset?: number;\n  asChild?: boolean;\n  withNavigation?: boolean;\n  scrollStep?: number;\n  scrollTriggerMode?: \"press\" | \"hover\" | \"click\";\n}\n\nfunction Scroller(props: ScrollerProps) {\n  const {\n    orientation = \"vertical\",\n    hideScrollbar,\n    className,\n    size = 40,\n    offset = 0,\n    scrollStep = 40,\n    style,\n    asChild,\n    withNavigation = false,\n    scrollTriggerMode = \"press\",\n    ref,\n    ...scrollerProps\n  } = props;\n\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n  const composedRef = useComposedRefs(ref, containerRef);\n  const [scrollVisibility, setScrollVisibility] =\n    React.useState<ScrollVisibility>({\n      up: false,\n      down: false,\n      left: false,\n      right: false,\n    });\n\n  const onScrollBy = React.useCallback(\n    (direction: ScrollDirection) => {\n      const container = containerRef.current;\n      if (!container) return;\n\n      const scrollMap: Record<ScrollDirection, () => void> = {\n        up: () => (container.scrollTop -= scrollStep),\n        down: () => (container.scrollTop += scrollStep),\n        left: () => (container.scrollLeft -= scrollStep),\n        right: () => (container.scrollLeft += scrollStep),\n      };\n\n      scrollMap[direction]();\n    },\n    [scrollStep],\n  );\n\n  const scrollHandlers = React.useMemo(\n    () => ({\n      up: () => onScrollBy(\"up\"),\n      down: () => onScrollBy(\"down\"),\n      left: () => onScrollBy(\"left\"),\n      right: () => onScrollBy(\"right\"),\n    }),\n    [onScrollBy],\n  );\n\n  React.useLayoutEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    function onScroll() {\n      if (!container) return;\n\n      const isVertical = orientation === \"vertical\";\n\n      if (isVertical) {\n        const scrollTop = container.scrollTop;\n        const clientHeight = container.clientHeight;\n        const scrollHeight = container.scrollHeight;\n\n        if (withNavigation) {\n          setScrollVisibility((prev) => {\n            const newUp = scrollTop > offset;\n            const newDown = scrollTop + clientHeight < scrollHeight;\n\n            if (prev.up !== newUp || prev.down !== newDown) {\n              return {\n                ...prev,\n                up: newUp,\n                down: newDown,\n              };\n            }\n            return prev;\n          });\n        }\n\n        const hasTopScroll = scrollTop > offset;\n        const hasBottomScroll =\n          scrollTop + clientHeight + offset < scrollHeight;\n        const isVerticallyScrollable = scrollHeight > clientHeight;\n\n        if (hasTopScroll && hasBottomScroll && isVerticallyScrollable) {\n          container.setAttribute(DATA_TOP_BOTTOM_SCROLL, \"true\");\n          container.removeAttribute(DATA_TOP_SCROLL);\n          container.removeAttribute(DATA_BOTTOM_SCROLL);\n        } else {\n          container.removeAttribute(DATA_TOP_BOTTOM_SCROLL);\n          if (hasTopScroll) container.setAttribute(DATA_TOP_SCROLL, \"true\");\n          else container.removeAttribute(DATA_TOP_SCROLL);\n          if (hasBottomScroll && isVerticallyScrollable)\n            container.setAttribute(DATA_BOTTOM_SCROLL, \"true\");\n          else container.removeAttribute(DATA_BOTTOM_SCROLL);\n        }\n      }\n\n      const scrollLeft = container.scrollLeft;\n      const clientWidth = container.clientWidth;\n      const scrollWidth = container.scrollWidth;\n\n      if (withNavigation) {\n        setScrollVisibility((prev) => {\n          const newLeft = scrollLeft > offset;\n          const newRight = scrollLeft + clientWidth < scrollWidth;\n\n          if (prev.left !== newLeft || prev.right !== newRight) {\n            return {\n              ...prev,\n              left: newLeft,\n              right: newRight,\n            };\n          }\n          return prev;\n        });\n      }\n\n      const hasLeftScroll = scrollLeft > offset;\n      const hasRightScroll = scrollLeft + clientWidth + offset < scrollWidth;\n      const isHorizontallyScrollable = scrollWidth > clientWidth;\n\n      if (hasLeftScroll && hasRightScroll && isHorizontallyScrollable) {\n        container.setAttribute(DATA_LEFT_RIGHT_SCROLL, \"true\");\n        container.removeAttribute(DATA_LEFT_SCROLL);\n        container.removeAttribute(DATA_RIGHT_SCROLL);\n      } else {\n        container.removeAttribute(DATA_LEFT_RIGHT_SCROLL);\n        if (hasLeftScroll) container.setAttribute(DATA_LEFT_SCROLL, \"true\");\n        else container.removeAttribute(DATA_LEFT_SCROLL);\n        if (hasRightScroll && isHorizontallyScrollable)\n          container.setAttribute(DATA_RIGHT_SCROLL, \"true\");\n        else container.removeAttribute(DATA_RIGHT_SCROLL);\n      }\n    }\n\n    onScroll();\n    container.addEventListener(\"scroll\", onScroll);\n    window.addEventListener(\"resize\", onScroll);\n\n    return () => {\n      container.removeEventListener(\"scroll\", onScroll);\n      window.removeEventListener(\"resize\", onScroll);\n    };\n  }, [orientation, offset, withNavigation]);\n\n  const composedStyle = React.useMemo<React.CSSProperties>(\n    () => ({\n      \"--scroll-shadow-size\": `${size}px`,\n      ...style,\n    }),\n    [size, style],\n  );\n\n  const activeDirections = React.useMemo<ScrollDirection[]>(() => {\n    if (!withNavigation) return [];\n    return orientation === \"vertical\" ? [\"up\", \"down\"] : [\"left\", \"right\"];\n  }, [orientation, withNavigation]);\n\n  const ScrollerPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  const ScrollerImpl = (\n    <ScrollerPrimitive\n      data-slot=\"scroller\"\n      {...scrollerProps}\n      ref={composedRef}\n      style={composedStyle}\n      className={cn(\n        scrollerVariants({ orientation, hideScrollbar, className }),\n      )}\n    />\n  );\n\n  const navigationButtons = React.useMemo(() => {\n    if (!withNavigation) return null;\n\n    return activeDirections\n      .filter((direction) => scrollVisibility[direction])\n      .map((direction) => (\n        <ScrollButton\n          key={direction}\n          data-slot=\"scroll-button\"\n          direction={direction}\n          onClick={scrollHandlers[direction]}\n          triggerMode={scrollTriggerMode}\n        />\n      ));\n  }, [\n    activeDirections,\n    scrollVisibility,\n    scrollHandlers,\n    scrollTriggerMode,\n    withNavigation,\n  ]);\n\n  if (withNavigation) {\n    return (\n      <div className=\"relative w-full\">\n        {navigationButtons}\n        {ScrollerImpl}\n      </div>\n    );\n  }\n\n  return ScrollerImpl;\n}\n\nconst scrollButtonVariants = cva(\n  \"absolute z-10 transition-opacity [&>svg]:size-4 [&>svg]:opacity-80 hover:[&>svg]:opacity-100\",\n  {\n    variants: {\n      direction: {\n        up: \"top-2 left-1/2 -translate-x-1/2\",\n        down: \"bottom-2 left-1/2 -translate-x-1/2\",\n        left: \"top-1/2 left-2 -translate-y-1/2\",\n        right: \"top-1/2 right-2 -translate-y-1/2\",\n      },\n    },\n    defaultVariants: {\n      direction: \"up\",\n    },\n  },\n);\n\nconst directionToIcon: Record<ScrollDirection, React.ElementType> = {\n  up: ChevronUp,\n  down: ChevronDown,\n  left: ChevronLeft,\n  right: ChevronRight,\n} as const;\n\ninterface ScrollButtonProps extends React.ComponentProps<\"button\"> {\n  direction: ScrollDirection;\n  triggerMode?: \"press\" | \"hover\" | \"click\";\n}\n\nfunction ScrollButton(props: ScrollButtonProps) {\n  const {\n    direction,\n    className,\n    triggerMode = \"press\",\n    onClick,\n    ref,\n    ...buttonProps\n  } = props;\n\n  const [autoScrollTimer, setAutoScrollTimer] = React.useState<number | null>(\n    null,\n  );\n\n  const onAutoScrollStart = React.useCallback(\n    (event?: React.MouseEvent<HTMLButtonElement>) => {\n      if (autoScrollTimer !== null) return;\n\n      if (triggerMode === \"press\") {\n        const timer = window.setInterval(onClick ?? (() => {}), 50);\n        setAutoScrollTimer(timer);\n      } else if (triggerMode === \"hover\") {\n        const timer = window.setInterval(() => {\n          if (event) onClick?.(event);\n        }, 50);\n        setAutoScrollTimer(timer);\n      }\n    },\n    [autoScrollTimer, onClick, triggerMode],\n  );\n\n  const onAutoScrollStop = React.useCallback(() => {\n    if (autoScrollTimer === null) return;\n\n    window.clearInterval(autoScrollTimer);\n    setAutoScrollTimer(null);\n  }, [autoScrollTimer]);\n\n  const eventHandlers = React.useMemo(() => {\n    const triggerModeHandlers: Record<\n      NonNullable<ScrollerProps[\"scrollTriggerMode\"]>,\n      React.ComponentProps<\"button\">\n    > = {\n      press: {\n        onPointerDown: onAutoScrollStart,\n        onPointerUp: onAutoScrollStop,\n        onPointerLeave: onAutoScrollStop,\n        onClick: () => {},\n      },\n      hover: {\n        onPointerEnter: onAutoScrollStart,\n        onPointerLeave: onAutoScrollStop,\n        onClick: () => {},\n      },\n      click: {\n        onClick,\n      },\n    } as const;\n\n    return triggerModeHandlers[triggerMode] ?? {};\n  }, [triggerMode, onAutoScrollStart, onAutoScrollStop, onClick]);\n\n  React.useEffect(() => {\n    return () => onAutoScrollStop();\n  }, [onAutoScrollStop]);\n\n  const Icon = directionToIcon[direction];\n\n  return (\n    <button\n      type=\"button\"\n      {...buttonProps}\n      {...eventHandlers}\n      ref={ref}\n      className={cn(scrollButtonVariants({ direction, className }))}\n    >\n      <Icon />\n    </button>\n  );\n}\n\nexport { Scroller };\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}