import { useEffect, useState } from 'react' /** * Which section the reader is currently in, plus how far through the page they * are. Driven by scroll position rather than by an IntersectionObserver: every * section here is taller than a phone viewport, so the question is not "is this * visible" but "which one is under the reading line", and a threshold-based * observer cannot answer that. * * `hrefs` must be a stable array — pass a module-level constant. */ export function useReadingPosition(hrefs: readonly string[]) { const [index, setIndex] = useState(0) const [progress, setProgress] = useState(0) useEffect(() => { let frame = 0 const measure = () => { frame = 0 /* A third of the way down the screen: the line the eye actually reads from, not the very top edge. */ const line = window.scrollY + window.innerHeight * 0.3 let current = 0 hrefs.forEach((href, position) => { const element = document.querySelector(href) if (element && element.getBoundingClientRect().top + window.scrollY <= line) current = position }) const scrollable = document.documentElement.scrollHeight - window.innerHeight setIndex(current) setProgress(scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 0) } const onScroll = () => { if (!frame) frame = requestAnimationFrame(measure) } measure() window.addEventListener('scroll', onScroll, { passive: true }) window.addEventListener('resize', onScroll) return () => { window.removeEventListener('scroll', onScroll) window.removeEventListener('resize', onScroll) if (frame) cancelAnimationFrame(frame) } }, [hrefs]) return { index, progress } }