{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-tab-bar-fade", "type": "registry:hook", "title": "useTabBarFade", "description": "Edge-fade masks for a horizontally overflowing tab strip; returns the CSS class to apply.", "categories": [ "hooks" ], "registryDependencies": [ "https://whiskeyjack.net/r/direction.json" ], "files": [ { "path": "hooks/use-tab-bar-fade.ts", "type": "registry:hook", "target": "hooks/use-tab-bar-fade.ts", "content": "import { useEffect, useState, type RefObject } from 'react'\nimport { isRTL } from '@/lib/direction'\n\nexport type FadeClass = '' | 'tab-bar-fade-left' | 'tab-bar-fade-right' | 'tab-bar-fade-both'\n\n/**\n * Tracks whether the given scroll container has overflow content to the\n * left, right, both, or neither. Returns the CSS class name to apply.\n *\n * Listens to scroll on the container, plus resize (window + ResizeObserver\n * on the container) so the class updates when the container size or its\n * children change.\n *\n * `changeKey` is an optional value that, when it changes, forces the\n * effect to re-run. Use this when the scroll container element is\n * conditionally rendered by a parent (e.g., `{tabs.length >= 2 &&
}`):\n * pass `tabs.length` (or any signal that the container has now mounted)\n * so the hook can attach its observers once the ref points at a real\n * element.\n */\nexport function useTabBarFade(ref: RefObject, changeKey?: unknown): FadeClass {\n const [fade, setFade] = useState('')\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const update = () => {\n const { scrollLeft, scrollWidth, clientWidth } = el\n const maxScroll = scrollWidth - clientWidth\n // Normalize across the LTR (0..max) and modern-RTL (0..-max) scrollLeft\n // conventions: `pos` is 0 at the START edge and grows toward the END.\n const pos = Math.abs(scrollLeft)\n // 1px tolerance handles sub-pixel rendering at the extreme edges.\n const canStart = pos > 1\n const canEnd = pos < maxScroll - 1\n // Map the logical start/end to the physical mask class per direction: the\n // start edge is on the left in LTR, on the right in RTL.\n const rtl = isRTL(el)\n const startClass: FadeClass = rtl ? 'tab-bar-fade-right' : 'tab-bar-fade-left'\n const endClass: FadeClass = rtl ? 'tab-bar-fade-left' : 'tab-bar-fade-right'\n const cls: FadeClass =\n canStart && canEnd ? 'tab-bar-fade-both'\n : canStart ? startClass\n : canEnd ? endClass\n : ''\n setFade(prev => prev === cls ? prev : cls)\n }\n\n update()\n el.addEventListener('scroll', update, { passive: true })\n const ro = new ResizeObserver(update)\n ro.observe(el)\n // Also observe each child -- a scrollable container's own size doesn't\n // change when overflow children are added, but ResizeObserver on the\n // children catches their sizing (e.g., async-loaded tab labels).\n Array.from(el.children).forEach(child => ro.observe(child))\n // MutationObserver catches add/remove of tab elements (e.g., when the\n // data snapshot arrives and renders the tabs).\n const mo = new MutationObserver(() => {\n Array.from(el.children).forEach(child => ro.observe(child))\n update()\n })\n mo.observe(el, { childList: true })\n window.addEventListener('resize', update)\n\n return () => {\n el.removeEventListener('scroll', update)\n ro.disconnect()\n mo.disconnect()\n window.removeEventListener('resize', update)\n }\n }, [ref, changeKey])\n\n return fade\n}\n" } ], "docs": "TabBar integrates this already. Use it directly only for a custom horizontal strip. It maps the logical start/end fade edge to the physical mask class under RTL, and the fade applies at md+ only.", "meta": { "group": "hooks", "related": [ "tab-bar", "use-scroll-fade" ], "exports": [ "useTabBarFade" ], "siteSlug": "use-tab-bar-fade" } }