import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; const FADE = 28; // px of taper at each overflowing edge /** * Horizontally scrollable row of chips/tabs. Fades whichever edge still has * content behind it, so a clipped item reads as "scroll for more" rather than * as a layout bug — and contains its overscroll so a sideways swipe never * triggers the browser's back gesture. */ export function ScrollRow({ children, className = "", wrapperClassName = "", bleed = true, }: { children: ReactNode; /** Applied to the scrolling element (layout of the items). */ className?: string; /** Applied to the positioned wrapper (borders, margins). */ wrapperClassName?: string; /** Extend to the screen edges on phones so the row reads as scrollable. */ bleed?: boolean; }) { const ref = useRef(null); const [edges, setEdges] = useState({ start: false, end: false }); const update = useCallback(() => { const el = ref.current; if (!el) return; const max = el.scrollWidth - el.clientWidth; setEdges({ start: el.scrollLeft > 4, end: max > 4 && el.scrollLeft < max - 4 }); }, []); useEffect(() => { update(); const el = ref.current; if (!el || typeof ResizeObserver === "undefined") return; const ro = new ResizeObserver(update); ro.observe(el); for (const child of Array.from(el.children)) ro.observe(child); return () => ro.disconnect(); }, [update]); const mask = `linear-gradient(to right, ${ edges.start ? `transparent 0, black ${FADE}px` : "black 0" }, ${edges.end ? `black calc(100% - ${FADE}px), transparent 100%` : "black 100%"})`; return (
{children}
); }