"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; type TOCItem = { title: ReactNode; url: string; depth: number; }; const MAX_LINE_WIDTH = 36; // h2 active width, used as fixed container width function getLineWidth(depth: number, isActive: boolean): number { const base = depth <= 2 ? 24 : depth === 3 ? 16 : 10; return isActive ? base + 12 : base; } export function BlogTOC({ items }: { items: TOCItem[] }) { const [activeId, setActiveId] = useState(null); const [isHovered, setIsHovered] = useState(false); const listRef = useRef(null); const isClickScrolling = useRef(false); const clickTimer = useRef>(null); useEffect(() => { if (items.length === 0) return; const headingIds = items.map((item) => item.url.slice(1)); const observer = new IntersectionObserver( (entries) => { if (isClickScrolling.current) return; for (const entry of entries) { if (entry.isIntersecting) { setActiveId(entry.target.id); break; } } }, { rootMargin: "-80px 0px -70% 0px", threshold: 0, }, ); for (const id of headingIds) { const element = document.getElementById(id); if (element) { observer.observe(element); } } return () => observer.disconnect(); }, [items]); const handleClick = (id: string) => { setActiveId(id); isClickScrolling.current = true; if (clickTimer.current) clearTimeout(clickTimer.current); clickTimer.current = setTimeout(() => { isClickScrolling.current = false; }, 800); }; if (items.length === 0) return null; return ( ); }