import { useEffect, useRef, type ReactNode, type RefObject } from 'react'; import styles from './InfiniteScrollArea.module.css'; export interface InfiniteScrollAreaProps { onEndReached: () => void; onTopVisibilityChange?: (atTop: boolean) => void; isFetchingNextPage: boolean; hasNextPage: boolean; isLoading?: boolean; hasItems?: boolean; maxHeight?: number | string; children: ReactNode; /** Optional caller-owned scroll container ref (e.g. for scroll-to-top on refresh). */ scrollRef?: RefObject; className?: string; } export function InfiniteScrollArea({ onEndReached, onTopVisibilityChange, isFetchingNextPage, hasNextPage, isLoading = false, hasItems = true, maxHeight = 360, children, scrollRef, className, }: InfiniteScrollAreaProps) { const internalRef = useRef(null); const containerRef = scrollRef ?? internalRef; const topSentinel = useRef(null); const bottomSentinel = useRef(null); useEffect(() => { if (!onTopVisibilityChange) return; const root = containerRef.current; const target = topSentinel.current; if (!root || !target) return; const obs = new IntersectionObserver( (entries) => { for (const e of entries) onTopVisibilityChange(e.isIntersecting); }, { root, threshold: 1.0 }, ); obs.observe(target); return () => obs.disconnect(); }, [containerRef, onTopVisibilityChange]); useEffect(() => { if (!hasNextPage) return; const root = containerRef.current; const target = bottomSentinel.current; if (!root || !target) return; const obs = new IntersectionObserver( (entries) => { for (const e of entries) if (e.isIntersecting) onEndReached(); }, { root, rootMargin: '100px', threshold: 0 }, ); obs.observe(target); return () => obs.disconnect(); }, [containerRef, onEndReached, hasNextPage]); return (