{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-headroom", "title": "Use Headroom", "author": "designbycode", "dependencies": [ "react" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "resources/js/registry/new-york/hooks/use-headroom.ts", "content": "import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface ToleranceConfig {\n up: number;\n down: number;\n}\n\nexport interface UseHeadroomOptions {\n enabled?: boolean;\n offset?: number;\n tolerance?: number | ToleranceConfig;\n scroller?: Element | null;\n}\n\n/**\n * useHeadroom - A React hook that replicates headroom.js behavior.\n *\n * Returns an object with CSS class flags based on scroll direction and position,\n * plus a ref to attach to your header element (for scroll offset calculation).\n *\n * @param {Object} options\n * @param {boolean} [options.enabled=true] - Enable/disable the headroom behavior. When false, header is always pinned.\n * @param {number} [options.offset=0] - Scroll distance (px) before the hook activates.\n * @param {number} [options.tolerance=0] - Scroll delta (px) required to trigger a state change.\n * Can also be { up: number, down: number }.\n * @param {Element|null} [options.scroller] - Scrollable element to listen on (default: window).\n *\n * @returns {{\n * ref: React.RefObject, - Attach this to your header element.\n * pinned: boolean, - true when header should be visible (scroll up or at top).\n * unpinned: boolean, - true when header should be hidden (scroll down).\n * top: boolean, - true when at the very top (within offset).\n * notTop: boolean, - true when scrolled past offset.\n * bottom: boolean, - true when at the bottom of the page/scroller.\n * notBottom: boolean, - true when not at the bottom.\n * }}\n *\n */\nfunction useHeadroom({\n enabled = true,\n offset = 0,\n tolerance = 0,\n scroller = null,\n}: UseHeadroomOptions = {}) {\n const ref = useRef(null);\n\n const getInitialState = useCallback(\n () => ({\n pinned: true,\n unpinned: false,\n top: true,\n notTop: false,\n bottom: false,\n notBottom: true,\n }),\n [],\n );\n\n const [state, setState] = useState(getInitialState);\n\n // Normalize tolerance into { up, down }\n const getTolerance = useCallback((): ToleranceConfig => {\n if (typeof tolerance === 'number') {\n return { up: tolerance, down: tolerance };\n }\n\n return tolerance;\n }, [tolerance]);\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n const scrollEl = scroller ?? window;\n\n const getScrollY = () =>\n scrollEl instanceof Element\n ? scrollEl.scrollTop\n : (window.scrollY ?? window.pageYOffset);\n\n const getScrollHeight = () =>\n scrollEl instanceof Element\n ? scrollEl.scrollHeight\n : document.documentElement.scrollHeight;\n\n const getClientHeight = () =>\n scrollEl instanceof Element\n ? scrollEl.clientHeight\n : window.innerHeight;\n\n let lastScrollY = getScrollY();\n let ticking = false;\n\n const update = () => {\n const currentScrollY = getScrollY();\n const scrollHeight = getScrollHeight();\n const clientHeight = getClientHeight();\n const tolerances = getTolerance();\n\n const isTop = currentScrollY <= offset;\n const isBottom = currentScrollY + clientHeight >= scrollHeight - 1;\n const delta = currentScrollY - lastScrollY;\n const scrollingDown = delta > 0;\n const scrollingUp = delta < 0;\n\n // Determine pin/unpin only when tolerance is exceeded\n setState((prev) => {\n let pinned = prev.pinned;\n\n if (isTop) {\n // Always pin at the top\n pinned = true;\n } else if (\n scrollingDown &&\n Math.abs(delta) >= tolerances.down\n ) {\n pinned = false;\n } else if (scrollingUp && Math.abs(delta) >= tolerances.up) {\n pinned = true;\n }\n\n return {\n pinned,\n unpinned: !pinned,\n top: isTop,\n notTop: !isTop,\n bottom: isBottom,\n notBottom: !isBottom,\n };\n });\n\n lastScrollY = currentScrollY;\n ticking = false;\n };\n\n const onScroll = () => {\n if (!ticking) {\n requestAnimationFrame(update);\n ticking = true;\n }\n };\n\n scrollEl.addEventListener('scroll', onScroll, { passive: true });\n\n // Run once on mount to set initial state\n update();\n\n return () => {\n scrollEl.removeEventListener('scroll', onScroll);\n };\n }, [enabled, offset, getTolerance, scroller]);\n\n return { ref, ...state };\n}\n\nexport default useHeadroom;\n", "type": "registry:hook" } ], "meta": { "category": "hooks", "version": "1.0.0" }, "type": "registry:hook" }