{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "toc", "type": "registry:component", "title": "Table of Contents", "description": "Show your table of contents in a sidebar.", "files": [ { "path": "components/retroui/TableOfContents.tsx", "content": "\n\"use client\";\n\nimport { cn } from '@/lib/utils';\nimport React, { useEffect, useState } from 'react';\n\ninterface TOCItem {\n title: string;\n url: string;\n level: number;\n items?: TOCItem[];\n}\n\ninterface TableOfContentsProps {\n depth?: number;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction generateTOCFromDOM(depth: number = 6): TOCItem[] {\n const headings: NodeListOf = document.querySelectorAll(\n Array.from({ length: depth }, (_, i) => `h${i + 1}`).join(', ')\n );\n\n const items: TOCItem[] = [];\n const stack: TOCItem[] = [];\n\n headings.forEach((heading) => {\n const level = parseInt(heading.tagName.charAt(1));\n const id = heading.id || heading.textContent?.toLowerCase().replace(/\\s+/g, '-').replace(/[^\\w-]/g, '') || '';\n\n if (!heading.id && id) {\n heading.id = id;\n }\n\n const item: TOCItem = {\n title: heading.textContent || '',\n url: `#${id}`,\n level,\n };\n\n while (stack.length > 0 && stack[stack.length - 1].level >= level) {\n stack.pop();\n }\n\n if (stack.length === 0) {\n items.push(item);\n } else {\n const parent = stack[stack.length - 1];\n if (!parent.items) parent.items = [];\n parent.items.push(item);\n }\n\n stack.push(item);\n });\n\n return items;\n}\n\nfunction renderTOCItems(items: TOCItem[], level = 0, activeId: string | null) {\n if (!items || items.length === 0) return null;\n\n return (\n \n );\n}\n\nexport function TableOfContents({\n depth = 2,\n className = \"\",\n children,\n}: TableOfContentsProps) {\n const [tocItems, setTocItems] = useState([]);\n const [activeId, setActiveId] = useState(null);\n\n useEffect(() => {\n const items = generateTOCFromDOM(depth);\n setTocItems(items);\n\n const observer = new MutationObserver(() => {\n const updatedItems = generateTOCFromDOM(depth);\n setTocItems(updatedItems);\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['id']\n });\n\n return () => observer.disconnect();\n }, [depth]);\n\n useEffect(() => {\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setActiveId(entry.target.id);\n }\n });\n },\n { rootMargin: '-20% 0% -35% 0%' }\n );\n\n const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');\n headings.forEach((heading) => observer.observe(heading));\n\n return () => observer.disconnect();\n }, []);\n\n if (tocItems.length === 0) {\n return null;\n }\n\n return (\n
\n {children}\n {renderTOCItems(tocItems, 0, activeId)}\n
\n );\n}", "type": "registry:component", "target": "components/retroui/TableOfContents.tsx" } ] }