import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react'; interface ProgressiveRowsOptions { total: number; rootRef: RefObject; resetKey: unknown; enabled?: boolean; batchSize?: number; preserveOnDisable?: boolean; } /** Render a cumulative prefix, loading another batch near the actual scroll body's bottom. */ export function useProgressiveRows({ total, rootRef, resetKey, enabled = true, batchSize = 40, preserveOnDisable = false }: ProgressiveRowsOptions) { const resetEnabled = preserveOnDisable || enabled; const context = useMemo(() => ({ resetKey, enabled: resetEnabled, batchSize }), [resetKey, resetEnabled, batchSize]); const scrollContext = useRef(context); const [progress, setProgress] = useState(() => ({ context, limit: batchSize })); const [sentinel, setSentinel] = useState(null); const visibleCount = Math.min(total, progress.context === context ? progress.limit : batchSize); const hasMore = visibleCount < total; const loadMore = useCallback(() => { if (!enabled) return; setProgress(previous => ({ context, limit: Math.min(total, (previous.context === context ? previous.limit : batchSize) + batchSize) })); }, [enabled, context, total, batchSize]); useEffect(() => { if (enabled && rootRef.current && (!preserveOnDisable || scrollContext.current !== context)) rootRef.current.scrollTop = 0; scrollContext.current = context; }, [context, enabled, rootRef, preserveOnDisable]); useEffect(() => { const root = rootRef.current; if (!enabled || !hasMore || !root || !sentinel || typeof IntersectionObserver === 'undefined') return; let active = true; let queued = false; let frame: number | undefined; const observer = new IntersectionObserver(entries => { if (!active || queued || !entries.some(entry => entry.isIntersecting)) return; queued = true; // One append per observer: let layout settle before deciding whether more rows fit. frame = requestAnimationFrame(loadMore); }, { root, rootMargin: '0px 0px 240px 0px', threshold: 0 }); observer.observe(sentinel); return () => { active = false; observer.disconnect(); if (frame !== undefined) cancelAnimationFrame(frame); }; }, [enabled, hasMore, rootRef, sentinel, visibleCount, loadMore]); return { visibleCount, total, hasMore, loadMore, sentinelRef: setSentinel }; }