{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-of-contents",
  "title": "Table of Contents",
  "description": "A page outline that marks the section the reader is in.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/table-of-contents/table-of-contents.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type TocSection = {\n  id: string\n  label: string\n}\n\nexport type TableOfContentsProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"children\"\n> & {\n  sections: readonly TocSection[]\n  label?: string\n  /** How far below the top a heading counts as reached, in pixels. */\n  offset?: number\n  onActiveChange?: (id: string | null) => void\n}\n\nexport function TableOfContents({\n  sections,\n  label = \"On this page\",\n  offset = 96,\n  onActiveChange,\n  className,\n  ...rootProps\n}: TableOfContentsProps) {\n  const [activeId, setActiveId] = React.useState(sections[0]?.id ?? null)\n\n  const report = React.useRef(onActiveChange)\n  React.useEffect(() => {\n    report.current = onActiveChange\n  })\n\n  React.useEffect(() => {\n    let frame = 0\n\n    const update = () => {\n      frame = 0\n\n      const targets = sections\n        .map((section) => document.getElementById(section.id))\n        .filter((element): element is HTMLElement => element !== null)\n\n      if (targets.length === 0) return\n\n      // Sections are tall and uneven, so this tracks the heading most recently\n      // passed rather than whichever box happens to intersect a band.\n      let current = targets[0]!.id\n      for (const target of targets) {\n        if (target.getBoundingClientRect().top <= offset) current = target.id\n      }\n\n      // A short last section may never reach the line, so the end of the page\n      // selects it outright.\n      const atEnd =\n        window.innerHeight + window.scrollY >=\n        document.documentElement.scrollHeight - 2\n\n      const next = atEnd ? targets.at(-1)!.id : current\n\n      setActiveId((previous) => {\n        if (previous !== next) report.current?.(next)\n        return next\n      })\n    }\n\n    const schedule = () => {\n      if (frame) return\n      frame = window.requestAnimationFrame(update)\n    }\n\n    schedule()\n    window.addEventListener(\"scroll\", schedule, { passive: true })\n    window.addEventListener(\"resize\", schedule)\n\n    return () => {\n      window.cancelAnimationFrame(frame)\n      window.removeEventListener(\"scroll\", schedule)\n      window.removeEventListener(\"resize\", schedule)\n    }\n  }, [sections, offset])\n\n  return (\n    <nav\n      data-slot=\"table-of-contents\"\n      aria-label={label}\n      className={cn(\"grid gap-1.5\", className)}\n      {...rootProps}\n    >\n      <p className=\"text-foreground text-xs font-bold tracking-[0.06em]\">\n        {label}\n      </p>\n\n      {sections.map((section) => (\n        <a\n          key={section.id}\n          href={`#${section.id}`}\n          data-slot=\"table-of-contents-link\"\n          data-active={section.id === activeId || undefined}\n          aria-current={section.id === activeId ? \"location\" : undefined}\n          className={cn(\n            \"text-muted-foreground hover:text-foreground data-[active]:text-foreground py-0.5 text-sm no-underline transition-colors duration-150 data-[active]:font-semibold motion-reduce:transition-none\"\n          )}\n        >\n          {section.label}\n        </a>\n      ))}\n    </nav>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "docs",
    "navigation",
    "scroll"
  ],
  "type": "registry:ui"
}