{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scroll-shadow","type":"registry:ui","title":"Scroll Shadow","description":"A scroll area whose edges fade while there's more to see, instead of a scrollbar.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-motion.json","https://dashral.com/r/button.json"],"files":[{"path":"registry/dashral/scroll-shadow.tsx","type":"registry:file","target":"components/dashral/scroll-shadow.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { reducedMotion } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\nimport { Button } from \"./button\"\n\ntype ScrollShadowProps<T extends React.ElementType> = {\n  /** The element that scrolls. A `div` by default; pass `\"ul\"` for a list. */\n  as?: T\n  orientation?: \"vertical\" | \"horizontal\"\n  /** Length of the fade at each edge, in px. */\n  size?: number\n  /** Show the scrollbar as well as the fade. */\n  scrollbar?: boolean\n  /**\n   * Horizontal only: round ‹ › buttons at an edge that has more to show, for mouse users without a trackpad.\n   * Keyboard users reach hidden content by tabbing through it, which scrolls it into view.\n   */\n  arrows?: boolean\n  /** How far one arrow click scrolls, in px. Defaults to 80% of the visible width. */\n  step?: number\n  ref?: React.Ref<HTMLElement>\n} & Omit<React.ComponentPropsWithoutRef<T>, \"as\">\n\n/**\n * A scroll area whose edges fade out while there's more to scroll that way, so the fade itself says the\n * content continues. The scrollbar is hidden unless you ask for it; wheel, trackpad, touch and keyboard\n * scrolling all still work.\n */\nfunction ScrollShadow<T extends React.ElementType = \"div\">({\n  as,\n  orientation = \"vertical\",\n  size = 40,\n  scrollbar = false,\n  arrows = false,\n  step,\n  className,\n  style,\n  children,\n  ref,\n  ...props\n}: ScrollShadowProps<T>) {\n  const Component = (as ?? \"div\") as React.ElementType\n  const node = React.useRef<HTMLElement | null>(null)\n  const [edges, setEdges] = React.useState({ start: false, end: false })\n  // Right to left (Arabic), a row starts at the right and scrollLeft runs from 0 down to minus the overflow.\n  const [rtl, setRtl] = React.useState(false)\n  const horizontal = orientation === \"horizontal\"\n\n  React.useEffect(() => {\n    const el = node.current\n    if (!el) return\n    const update = () => {\n      const isRtl = horizontal && getComputedStyle(el).direction === \"rtl\"\n      setRtl(isRtl)\n      const position = horizontal ? Math.abs(el.scrollLeft) : el.scrollTop\n      const max = horizontal ? el.scrollWidth - el.clientWidth : el.scrollHeight - el.clientHeight\n      const start = position > 1\n      const end = position < max - 1\n      setEdges((current) => (current.start === start && current.end === end ? current : { start, end }))\n    }\n    update()\n    el.addEventListener(\"scroll\", update, { passive: true })\n    // A resize (window, sidebar) or new content (an item added) changes what's out of view.\n    const resize = new ResizeObserver(update)\n    resize.observe(el)\n    const mutations = new MutationObserver(update)\n    mutations.observe(el, { childList: true, subtree: true })\n    return () => {\n      el.removeEventListener(\"scroll\", update)\n      resize.disconnect()\n      mutations.disconnect()\n    }\n  }, [horizontal])\n\n  const fade = `${size}px`\n  const mask = `linear-gradient(${horizontal ? (rtl ? \"to left\" : \"to right\") : \"to bottom\"}, ${edges.start ? \"transparent\" : \"#000\"}, #000 ${fade}, #000 calc(100% - ${fade}), ${edges.end ? \"transparent\" : \"#000\"})`\n\n  const area = (\n    <Component\n      data-slot=\"scroll-shadow\"\n      data-orientation={orientation}\n      {...props}\n      ref={(el: HTMLElement | null) => {\n        node.current = el\n        if (typeof ref === \"function\") ref(el)\n        else if (ref) (ref as React.RefObject<HTMLElement | null>).current = el\n      }}\n      className={cn(\n        horizontal ? \"overflow-x-auto\" : \"overflow-y-auto\",\n        !scrollbar && \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        className\n      )}\n      style={{ ...style, maskImage: mask, WebkitMaskImage: mask }}\n    >\n      {children}\n    </Component>\n  )\n\n  if (!horizontal || !arrows) return area\n\n  const scroll = (direction: 1 | -1) => {\n    const el = node.current\n    if (!el) return\n    const smooth = !reducedMotion()\n    // Toward the end is to the left when the row reads right to left.\n    el.scrollBy({ left: direction * (rtl ? -1 : 1) * (step ?? el.clientWidth * 0.8), behavior: smooth ? \"smooth\" : \"auto\" })\n  }\n\n  const arrow = \"absolute top-1/2 z-10 -translate-y-1/2 rounded-full bg-background shadow-sm\"\n  return (\n    <div data-slot=\"scroll-shadow-arrows\" className=\"relative min-w-0\">\n      {area}\n      {edges.start && (\n        <Button variant=\"outline\" size=\"icon-sm\" tabIndex={-1} aria-hidden onClick={() => scroll(-1)} className={cn(arrow, \"start-1\")}>\n          <ChevronLeftIcon className=\"rtl:-scale-x-100\" />\n        </Button>\n      )}\n      {edges.end && (\n        <Button variant=\"outline\" size=\"icon-sm\" tabIndex={-1} aria-hidden onClick={() => scroll(1)} className={cn(arrow, \"end-1\")}>\n          <ChevronRightIcon className=\"rtl:-scale-x-100\" />\n        </Button>\n      )}\n    </div>\n  )\n}\n\nexport { ScrollShadow }\n"}]}