{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-of-contents",
  "type": "registry:ui",
  "title": "Table of contents",
  "description": "Sticky in-page navigation with scroll-spy range highlighting.",
  "dependencies": [],
  "registryDependencies": [
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/track-highlight.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/table-of-contents-knobs.ts",
      "target": "@components/control-ui/knob-contracts/table-of-contents-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/table-of-contents.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const tableOfContentsKnobs = [\n  \"--cui-table-of-contents-rail-background\",\n  \"--cui-table-of-contents-highlight-radius\",\n  \"--cui-table-of-contents-highlight-background\",\n] as const;\nexport type TableOfContentsKnobStyle = Partial<Record<(typeof tableOfContentsKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/table-of-contents.css",
      "target": "@components/control-ui/styles/recipes/table-of-contents.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"root\"]) {\n    --cui-track-highlight-transition-duration: var(--duration-base);\n    --cui-table-of-contents-rail-background: oklch(from var(--border) l c h / 0.6);\n    --cui-table-of-contents-highlight-radius: var(--radius-md);\n    --cui-table-of-contents-highlight-background: oklch(from var(--foreground) l c h / 0.04);\n    border: 1px solid oklch(from var(--border) l c h / 0.7);\n    border-radius: var(--radius-panel);\n    background-color: oklch(from var(--card) l c h / 0.8);\n    box-shadow: var(--shadow-sm);\n    backdrop-filter: blur(var(--blur-sm));\n    font-size: var(--text-sm);\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"label\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-caption);\n    font-weight: 500;\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"rail\"]) {\n    background-color: var(--cui-table-of-contents-rail-background);\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"item\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-sm);\n    font-weight: 400;\n    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-standard);\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"item\"][data-depth=\"0\"]) {\n    font-weight: 500;\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"table-of-contents\"][data-slot=\"item\"]:hover) {\n      color: var(--foreground);\n    }\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"item\"]:focus-visible) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"item\"][data-active]) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"root\"] [data-control-family=\"track-highlight\"][data-slot=\"root\"]) {\n    --cui-track-highlight-radius: var(--cui-table-of-contents-highlight-radius);\n    --cui-track-highlight-background: var(--cui-table-of-contents-highlight-background);\n    --cui-track-highlight-ring-color: transparent;\n    --cui-track-highlight-shadow: none;\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"root\"][data-variant=\"trail\"]) {\n    --cui-table-of-contents-highlight-radius: 0px;\n    --cui-table-of-contents-highlight-background: transparent;\n  }\n\n  :where([data-control-family=\"table-of-contents\"][data-slot=\"trail\"]) {\n    border-radius: 9999px;\n    background-color: var(--foreground);\n  }\n}\n\n@property --cui-table-of-contents-rail-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-table-of-contents-highlight-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-table-of-contents-highlight-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/ui/table-of-contents.tsx",
      "target": "@components/control-ui/ui/table-of-contents.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport type { ComponentProps, CSSProperties } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { TrackHighlight } from \"@/components/control-ui/extensions/track-highlight\";\nimport type { TableOfContentsKnobStyle } from \"@/components/control-ui/knob-contracts/table-of-contents-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\n\nexport type TocItem = {\n  href: string;\n  label: string;\n  level?: number;\n  children?: TocItem[];\n};\n\nexport const tableOfContentsVariants = [\"background\", \"trail\", \"both\"] as const;\n\nexport type TableOfContentsVariant = (typeof tableOfContentsVariants)[number];\n\nexport type TableOfContentsProps = Omit<Omit<ComponentProps<\"nav\">, \"children\">, \"style\"> & {\n  style?: CSSProperties & TableOfContentsKnobStyle;\n} & {\n  items: TocItem[];\n  label?: string;\n  variant?: TableOfContentsVariant;\n};\n\nconst DETECTION_MARGIN = \"-80px 0px -20% 0px\";\nconst indentByDepth = [\"pl-3\", \"pl-5\", \"pl-7\", \"pl-9\", \"pl-11\", \"pl-14\"] as const;\n\nconst TOC_ITEM_SELECTOR = '[data-control-ui=\"table-of-contents\"][data-slot=\"item\"]';\nconst TOC_ACTIVE_SELECTOR = '[data-control-ui=\"table-of-contents\"][data-slot=\"item\"][data-active]';\n\nconst tableOfContentsItemClass = \"block py-1.5\";\nconst tableOfContentsHighlightClass = \"\";\nconst tableOfContentsTrailClass = \"absolute inset-y-0 left-0 w-0.5\";\n\nfunction getScrollContainer(el: HTMLElement): HTMLElement | null {\n  let node = el.parentElement;\n  while (node) {\n    if (/(auto|scroll|overlay)/.test(getComputedStyle(node).overflowY)) return node;\n    node = node.parentElement;\n  }\n  return null;\n}\n\nexport function useVisibleSections(ids: string[]): string[] {\n  const [visibleState, setVisibleState] = useState<{ idsKey: string; visibleIds: string[] }>({ idsKey: \"\", visibleIds: [] });\n  const idsKey = ids.join(\"|\");\n\n  useEffect(() => {\n    const idList = idsKey ? idsKey.split(\"|\") : [];\n    const elements = idList.map((id) => document.getElementById(id)).filter((el): el is HTMLElement => el !== null);\n    const firstElement = elements[0];\n    if (!firstElement) return;\n\n    const order = new Map(elements.map((el, index) => [el.id, index]));\n    const visible = new Set<string>();\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) visible.add(entry.target.id);\n          else visible.delete(entry.target.id);\n        }\n        setVisibleState({\n          idsKey,\n          visibleIds: [...visible].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)),\n        });\n      },\n      { root: getScrollContainer(firstElement), rootMargin: DETECTION_MARGIN, threshold: 0 },\n    );\n\n    for (const el of elements) observer.observe(el);\n    return () => observer.disconnect();\n  }, [idsKey]);\n\n  return visibleState.idsKey === idsKey ? visibleState.visibleIds : [];\n}\n\ntype TocNode = Omit<TableOfContentsProps[\"items\"][number], \"children\" | \"level\"> & {\n  depth: number;\n  level: number;\n  children?: TocNode[];\n};\n\nfunction collectLevels(items: TableOfContentsProps[\"items\"], fallbackLevel = 2): number[] {\n  return items.flatMap((item) => {\n    const level = item.level ?? fallbackLevel;\n    return [level, ...collectLevels(item.children ?? [], level + 1)];\n  });\n}\n\nfunction normalizeItems(items: TableOfContentsProps[\"items\"], baseLevel: number, fallbackLevel = 2, nestingDepth = 0): TocNode[] {\n  return items.map((item) => {\n    const level = item.level ?? fallbackLevel;\n    const depth = Math.max(nestingDepth, Math.max(0, level - baseLevel));\n    const nestedItems = item.children;\n    const children =\n      nestedItems && nestedItems.length > 0 ? normalizeItems(nestedItems, baseLevel, level + 1, nestingDepth + 1) : undefined;\n\n    return {\n      ...item,\n      level,\n      depth,\n      children,\n    };\n  });\n}\n\nfunction flattenItems(items: TocNode[]): TocNode[] {\n  return items.flatMap((item) => [item, ...flattenItems(item.children ?? [])]);\n}\n\nfunction itemIndentClass(depth: number) {\n  return indentByDepth[Math.min(depth, indentByDepth.length - 1)];\n}\n\nfunction activeIdsInRange(visibleIds: string[], targetIds: string[]) {\n  let firstIndex = targetIds.length;\n  let lastIndex = -1;\n\n  for (const id of visibleIds) {\n    const index = targetIds.indexOf(id);\n    if (index < 0) continue;\n    firstIndex = Math.min(firstIndex, index);\n    lastIndex = Math.max(lastIndex, index);\n  }\n\n  if (lastIndex < 0) return new Set<string>();\n  return new Set(targetIds.slice(firstIndex, lastIndex + 1));\n}\n\nexport function TableOfContents({ items, label = \"On this page\", variant = \"both\", className, style, ...props }: TableOfContentsProps) {\n  const baseLevel = Math.min(...collectLevels(items), 2);\n  const normalizedItems = normalizeItems(items, baseLevel);\n  const flatItems = flattenItems(normalizedItems);\n  const targetIds = flatItems.map((item) => item.href.replace(/^#/, \"\"));\n  const visibleIds = useVisibleSections(targetIds);\n  const activeSet = activeIdsInRange(visibleIds, targetIds);\n  const railStyle = style;\n  const highlightStyle = style;\n\n  if (items.length === 0) return null;\n\n  return (\n    <nav\n      data-control-ui=\"table-of-contents\"\n      data-control-family=\"table-of-contents\"\n      data-slot=\"root\"\n      data-variant={variant}\n      aria-label={label}\n      className={cn(\"sticky top-6 p-4\", className)}\n      style={style}\n      {...props}\n    >\n      <p data-control-ui=\"table-of-contents\" data-control-family=\"table-of-contents\" data-slot=\"label\" className=\"mb-3\">\n        {label}\n      </p>\n      <div data-control-ui=\"table-of-contents\" data-control-family=\"table-of-contents\" data-slot=\"track\" className=\"relative isolate\">\n        <span\n          aria-hidden\n          data-control-ui=\"table-of-contents\"\n          data-control-family=\"table-of-contents\"\n          data-slot=\"rail\"\n          className=\"pointer-events-none absolute inset-y-0 left-0 -z-20 w-px\"\n          style={railStyle}\n        />\n        <TrackHighlight\n          data-variant={variant}\n          itemSelector={TOC_ITEM_SELECTOR}\n          activeSelector={TOC_ACTIVE_SELECTOR}\n          range\n          followHover={false}\n          style={highlightStyle}\n          className={tableOfContentsHighlightClass}\n        >\n          {variant !== \"background\" && (\n            <div\n              aria-hidden\n              data-control-ui=\"table-of-contents\"\n              data-control-family=\"table-of-contents\"\n              data-slot=\"trail\"\n              className={tableOfContentsTrailClass}\n            />\n          )}\n        </TrackHighlight>\n        <TocList items={normalizedItems} activeSet={activeSet} variant={variant} root />\n      </div>\n    </nav>\n  );\n}\n\nfunction TocList({\n  items,\n  activeSet,\n  variant,\n  root = false,\n}: {\n  items: TocNode[];\n  activeSet: Set<string>;\n  variant: TableOfContentsVariant;\n  root?: boolean;\n}) {\n  return (\n    <ul\n      data-control-ui=\"table-of-contents\"\n      data-control-family=\"table-of-contents\"\n      data-slot=\"list\"\n      data-nested={root ? undefined : \"true\"}\n    >\n      {items.map((item) => {\n        const targetId = item.href.replace(/^#/, \"\");\n        const isActive = activeSet.has(targetId);\n\n        return (\n          <li key={item.href}>\n            <a\n              data-control-ui=\"table-of-contents\"\n              data-control-family=\"table-of-contents\"\n              data-slot=\"item\"\n              data-active={isActive || undefined}\n              data-variant={variant}\n              data-level={item.level}\n              data-depth={item.depth}\n              aria-current={isActive ? \"location\" : undefined}\n              href={item.href}\n              className={cn(tableOfContentsItemClass, itemIndentClass(item.depth))}\n            >\n              {item.label}\n            </a>\n            {item.children && <TocList items={item.children} activeSet={activeSet} variant={variant} />}\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/table-of-contents.css\"": {}
  },
  "meta": {}
}
