{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroller",
  "title": "Scroller",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/recipes.json",
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/scroller.tsx",
      "content": "\"use client\";\n\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\";\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { selectionFocus } from \"@/lib/recipes\";\n\nconst useIsoLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nexport type ScrollerOverflow = \"x\" | \"y\" | \"both\";\n\nexport interface ScrollerProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** Scroll axis. `x` for horizontal rails, `y` for stacked feeds, `both` for a grid canvas. */\n  overflow?: ScrollerOverflow;\n  /** Container height. Number → px, string → passed through. */\n  height?: number | string;\n  /** Container width. Number → px, string → passed through. Default `\"100%\"`. */\n  width?: number | string;\n  /**\n   * Fade the edge of the scroll region when there's more content in that\n   * direction (via `mask-image`). Default `true`. Only meaningful for\n   * `overflow=\"x\"` and `overflow=\"y\"`.\n   */\n  fade?: boolean;\n  /** Fade width in pixels. Default 32. */\n  fadeWidth?: number;\n  /** Class on the inner content wrapper: apply gap here for rail spacing. */\n  childrenContainerClassName?: string;\n  /** aria-label on the scroll region (best when there's no visible heading). */\n  ariaLabel?: string;\n  children?: React.ReactNode;\n}\n\n/**\n * Scroll container for overflowing lists. Wraps Base UI ScrollArea to get\n * a styled overlay scrollbar in vertical / bidirectional modes, and hides\n * it entirely in horizontal mode (edge fade masks are the \"more content\"\n * affordance for rails).\n *\n *   <Scroller overflow=\"x\" childrenContainerClassName=\"gap-3\">\n *     {items.map((item) => <Card key={item.id} className=\"shrink-0 w-64\">…</Card>)}\n *   </Scroller>\n */\nexport function Scroller({\n  overflow = \"x\",\n  height,\n  width,\n  fade = true,\n  fadeWidth = 32,\n  childrenContainerClassName,\n  ariaLabel,\n  className,\n  style,\n  children,\n  ...props\n}: ScrollerProps): React.ReactElement {\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const [canScrollBack, setCanScrollBack] = useState(false);\n  const [canScrollForward, setCanScrollForward] = useState(false);\n\n  const horizontal = overflow === \"x\" || overflow === \"both\";\n  const vertical = overflow === \"y\" || overflow === \"both\";\n\n  const updateAffordances = useCallback(() => {\n    const el = viewportRef.current;\n    if (!el) return;\n    if (overflow === \"y\") {\n      setCanScrollBack(el.scrollTop > 1);\n      setCanScrollForward(el.scrollTop + el.clientHeight < el.scrollHeight - 1);\n    } else {\n      setCanScrollBack(el.scrollLeft > 1);\n      setCanScrollForward(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);\n    }\n  }, [overflow]);\n\n  useIsoLayoutEffect(() => {\n    updateAffordances();\n    const el = viewportRef.current;\n    if (!el) return;\n    el.addEventListener(\"scroll\", updateAffordances, { passive: true });\n    const ro = new ResizeObserver(updateAffordances);\n    ro.observe(el);\n    return () => {\n      el.removeEventListener(\"scroll\", updateAffordances);\n      ro.disconnect();\n    };\n  }, [updateAffordances]);\n\n  const dimStyle: React.CSSProperties = {\n    ...(height != null && {\n      height: typeof height === \"number\" ? `${height}px` : height,\n    }),\n    ...(width != null && {\n      width: typeof width === \"number\" ? `${width}px` : width,\n    }),\n    ...style,\n  };\n\n  // Fade mask: gradient that fades to transparent at whichever edge has\n  // more content past it. Only applies to overflow=\"x\" / \"y\" (a 2-axis\n  // canvas would need a corner mask which is out of scope).\n  const maskStyle: React.CSSProperties = {};\n  if (fade && overflow !== \"both\") {\n    let stops: string | null = null;\n    if (overflow === \"x\") {\n      if (canScrollBack && canScrollForward) {\n        stops = `linear-gradient(to right, transparent 0, black ${fadeWidth}px, black calc(100% - ${fadeWidth}px), transparent 100%)`;\n      } else if (canScrollBack) {\n        stops = `linear-gradient(to right, transparent 0, black ${fadeWidth}px)`;\n      } else if (canScrollForward) {\n        stops = `linear-gradient(to right, black calc(100% - ${fadeWidth}px), transparent 100%)`;\n      }\n    } else if (overflow === \"y\") {\n      if (canScrollBack && canScrollForward) {\n        stops = `linear-gradient(to bottom, transparent 0, black ${fadeWidth}px, black calc(100% - ${fadeWidth}px), transparent 100%)`;\n      } else if (canScrollBack) {\n        stops = `linear-gradient(to bottom, transparent 0, black ${fadeWidth}px)`;\n      } else if (canScrollForward) {\n        stops = `linear-gradient(to bottom, black calc(100% - ${fadeWidth}px), transparent 100%)`;\n      }\n    }\n    if (stops) {\n      maskStyle.maskImage = stops;\n      (maskStyle as unknown as { WebkitMaskImage: string }).WebkitMaskImage = stops;\n    }\n  }\n\n  const innerFlexClass =\n    horizontal && !vertical\n      ? \"inline-flex\"\n      : vertical && !horizontal\n        ? \"flex flex-col\"\n        : \"grid\";\n\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroller\"\n      data-overflow={overflow}\n      className={cn(\"relative w-full\", className)}\n      style={dimStyle}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={viewportRef}\n        role=\"region\"\n        aria-label={ariaLabel}\n        tabIndex={0}\n        data-slot=\"scroller-viewport\"\n        className={cn(\n          \"size-full [-webkit-overflow-scrolling:touch]\",\n          selectionFocus,\n        )}\n        style={maskStyle}\n      >\n        <ScrollAreaPrimitive.Content\n          data-slot=\"scroller-content\"\n          className={cn(innerFlexClass, childrenContainerClassName)}\n        >\n          {children}\n        </ScrollAreaPrimitive.Content>\n      </ScrollAreaPrimitive.Viewport>\n      {vertical && (\n        <ScrollAreaPrimitive.Scrollbar\n          orientation=\"vertical\"\n          data-slot=\"scroller-scrollbar\"\n          className={cn(\n            \"absolute end-0.5 top-0.5 bottom-0.5 flex w-1.5 touch-none select-none\",\n            \"opacity-0 transition-opacity duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n            \"data-[hovering]:opacity-100 data-[scrolling]:opacity-100\",\n          )}\n        >\n          <ScrollAreaPrimitive.Thumb className=\"relative flex-1 rounded-full bg-ink/25 hover:bg-ink/40\" />\n        </ScrollAreaPrimitive.Scrollbar>\n      )}\n    </ScrollAreaPrimitive.Root>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/scroller.tsx"
    }
  ],
  "type": "registry:ui"
}