{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toc",
  "type": "registry:component",
  "title": "Table of Contents",
  "description": "A table of contents component with active anchor tracking and depth-aware line styling. Features two variants: default (straight line) and clerk (depth-responsive line that follows hierarchy).",
  "dependencies": [],
  "files": [
    {
      "path": "registry/abui/ui/toc.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  createContext,\n  useContext,\n  useRef,\n  useState,\n  useEffect,\n  useMemo,\n  useLayoutEffect,\n  type ReactNode,\n  type RefObject,\n  type ComponentProps,\n  type HTMLAttributes,\n} from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TOCItemType {\n  title: ReactNode\n  url: string\n  depth: number\n}\n\nexport type TableOfContents = TOCItemType[]\n\n// ============================================================================\n// Contexts\n// ============================================================================\n\nconst ActiveAnchorContext = createContext<string[]>([])\nconst ScrollContext = createContext<RefObject<HTMLElement | null>>({ current: null })\nconst TOCContext = createContext<TOCItemType[]>([])\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * The id of visible anchors\n */\nexport function useActiveAnchors(): string[] {\n  return useContext(ActiveAnchorContext)\n}\n\n/**\n * The estimated active heading ID\n */\nexport function useActiveAnchor(): string | undefined {\n  return useContext(ActiveAnchorContext)[0]\n}\n\n/**\n * Get TOC items from context\n */\nexport function useTOCItems(): TOCItemType[] {\n  return useContext(TOCContext)\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nfunction mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {\n  return value => {\n    refs.forEach(ref => {\n      if (typeof ref === \"function\") {\n        ref(value)\n      } else if (ref != null) {\n        ;(ref as React.MutableRefObject<T | null>).current = value\n      }\n    })\n  }\n}\n\n/**\n * Find the active heading of page using IntersectionObserver\n */\nfunction useAnchorObserver(watch: string[], single: boolean): string[] {\n  const observerRef = useRef<IntersectionObserver | null>(null)\n  const [activeAnchor, setActiveAnchor] = useState<string[]>([])\n  const stateRef = useRef<{ visible: Set<string> } | null>(null)\n\n  const onChange = React.useCallback(\n    (entries: IntersectionObserverEntry[]) => {\n      stateRef.current ??= { visible: new Set() }\n      const state = stateRef.current\n\n      for (const entry of entries) {\n        if (entry.isIntersecting) {\n          state.visible.add(entry.target.id)\n        } else {\n          state.visible.delete(entry.target.id)\n        }\n      }\n\n      if (state.visible.size === 0) {\n        const viewTop = entries[0]?.rootBounds?.top ?? 0\n        let fallback: Element | undefined\n        let min = -1\n\n        for (const id of watch) {\n          const element = document.getElementById(id)\n          if (!element) continue\n\n          const d = Math.abs(viewTop - element.getBoundingClientRect().top)\n          if (min === -1 || d < min) {\n            fallback = element\n            min = d\n          }\n        }\n\n        setActiveAnchor(fallback ? [fallback.id] : [])\n      } else {\n        const items = watch.filter(item => state.visible.has(item))\n        setActiveAnchor(single ? items.slice(0, 1) : items)\n      }\n    },\n    [watch, single],\n  )\n\n  useEffect(() => {\n    if (observerRef.current) return\n    observerRef.current = new IntersectionObserver(onChange, {\n      rootMargin: \"0px\",\n      threshold: 0.98,\n    })\n\n    return () => {\n      observerRef.current?.disconnect()\n      observerRef.current = null\n    }\n  }, [onChange])\n\n  useEffect(() => {\n    const observer = observerRef.current\n    if (!observer) return\n    const elements = watch.flatMap(heading => document.getElementById(heading) ?? [])\n\n    for (const element of elements) observer.observe(element)\n    return () => {\n      for (const element of elements) observer.unobserve(element)\n    }\n  }, [watch])\n\n  return activeAnchor\n}\n\n// ============================================================================\n// TOCProvider - Main context provider\n// ============================================================================\n\nexport interface TOCProviderProps {\n  toc: TableOfContents\n  /**\n   * Only accept one active item at most\n   * @defaultValue false\n   */\n  single?: boolean\n  children?: ReactNode\n}\n\nexport function TOCProvider({ toc, single = false, children }: TOCProviderProps) {\n  const headings = useMemo(() => {\n    return toc.map(item => item.url.split(\"#\")[1])\n  }, [toc])\n\n  const activeAnchors = useAnchorObserver(headings, single)\n\n  return (\n    <TOCContext.Provider value={toc}>\n      <ActiveAnchorContext.Provider value={activeAnchors}>{children}</ActiveAnchorContext.Provider>\n    </TOCContext.Provider>\n  )\n}\n\n// ============================================================================\n// TOCScrollArea - Scrollable container with auto-scroll support\n// ============================================================================\n\nexport function TOCScrollArea({ ref, className, ...props }: ComponentProps<\"div\">) {\n  const viewRef = useRef<HTMLDivElement>(null)\n\n  return (\n    <div\n      ref={mergeRefs(viewRef, ref)}\n      className={cn(\n        \"relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] [mask-image:linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3\",\n        className,\n      )}\n      {...props}\n    >\n      <ScrollContext.Provider value={viewRef}>{props.children}</ScrollContext.Provider>\n    </div>\n  )\n}\n\n// ============================================================================\n// TOCThumb - Active indicator that shows current position\n// ============================================================================\n\ntype TocThumbValues = [top: number, height: number]\n\ninterface TocThumbProps extends HTMLAttributes<HTMLDivElement> {\n  containerRef: RefObject<HTMLElement | null>\n}\n\nfunction calcThumb(container: HTMLElement, active: string[]): TocThumbValues {\n  if (active.length === 0 || container.clientHeight === 0) {\n    return [0, 0]\n  }\n\n  let upper = Number.MAX_VALUE\n  let lower = 0\n\n  for (const item of active) {\n    const element = container.querySelector<HTMLElement>(`a[href=\"#${item}\"]`)\n    if (!element) continue\n\n    const styles = getComputedStyle(element)\n    upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop))\n    lower = Math.max(lower, element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom))\n  }\n\n  return [upper, lower - upper]\n}\n\nfunction updateThumb(element: HTMLElement, info: TocThumbValues): void {\n  element.style.setProperty(\"--toc-top\", `${info[0]}px`)\n  element.style.setProperty(\"--toc-height\", `${info[1]}px`)\n}\n\nfunction TocThumb({ containerRef, ...props }: TocThumbProps) {\n  const thumbRef = useRef<HTMLDivElement>(null)\n  const active = useActiveAnchors()\n\n  useEffect(() => {\n    if (!containerRef.current) return\n    const container = containerRef.current\n\n    const onUpdate = () => {\n      if (!thumbRef.current) return\n      updateThumb(thumbRef.current, calcThumb(container, active))\n    }\n\n    const observer = new ResizeObserver(onUpdate)\n    observer.observe(container)\n    onUpdate()\n\n    return () => {\n      observer.disconnect()\n    }\n  }, [containerRef, active])\n\n  // Initial update\n  if (containerRef.current && thumbRef.current) {\n    updateThumb(thumbRef.current, calcThumb(containerRef.current, active))\n  }\n\n  return <div ref={thumbRef} role=\"none\" {...props} />\n}\n\n// ============================================================================\n// TOCItem - Individual TOC link item\n// ============================================================================\n\nexport interface TOCItemProps extends Omit<ComponentProps<\"a\">, \"href\"> {\n  href: string\n  onActiveChange?: (v: boolean) => void\n}\n\nexport function TOCItem({ ref, onActiveChange, ...props }: TOCItemProps) {\n  const containerRef = useContext(ScrollContext)\n  const anchorRef = useRef<HTMLAnchorElement>(null)\n  const activeAnchors = useActiveAnchors()\n  const activeOrder = activeAnchors.indexOf(props.href.slice(1))\n  const isActive = activeOrder !== -1\n  const shouldScroll = activeOrder === 0\n\n  useLayoutEffect(() => {\n    const anchor = anchorRef.current\n    const container = containerRef.current\n\n    // -------------------------------------------------------------------------\n    // CUSTOM SCROLL IMPLEMENTATION\n    // -------------------------------------------------------------------------\n    // This replaces the `scroll-into-view-if-needed` library used in fumadocs.\n    //\n    // LIBRARY ARCHITECTURE:\n    // - `scroll-into-view-if-needed`: Options parsing, scroll execution, scroll-margin support\n    // - `compute-scroll-into-view`: Core algorithm for boundary/position calculations\n    //\n    // The `compute-scroll-into-view` library provides sophisticated features:\n    // - `alignNearest`: Algorithm that calculates minimum scroll needed (with edge cases)\n    // - Scrollbar width/height handling in calculations\n    // - Border width accounting for containers\n    // - CSS transform scale handling\n    // - Visual viewport support (for pinch-zoom)\n    // - Multiple scrolling frames (scrolls all ancestors up to boundary)\n    //\n    // FEATURES WE DON'T IMPLEMENT (not needed for TOC use case):\n    // - Shadow DOM support via isInDocument() check\n    // - Custom scroll behavior callbacks (behavior: Function)\n    // - Multiple scroll targets (only need single container)\n    // - CSS scroll-margin-* properties (TOC items don't use these)\n    // - `block: 'nearest'` alignNearest algorithm (we always center)\n    // - Scrollbar width compensation (TOC uses scrollbar-width: none)\n    // - Border width compensation (TOC container has no borders)\n    // - CSS transform scale handling (TOC isn't scaled)\n    //\n    // FEATURES WE DO IMPLEMENT:\n    // - Element connectivity check (isConnected)\n    // - Boundary constraint (only scroll container, never page)\n    // - Smooth scrolling behavior\n    // - Center block alignment (block: 'center')\n    // - \"if-needed\" scroll mode (only scroll if out of view)\n    //\n    // ORIGINAL FUMADOCS IMPLEMENTATION:\n    // ```\n    // import scrollIntoView from 'scroll-into-view-if-needed'\n    //\n    // scrollIntoView(anchor, {\n    //   behavior: 'smooth',\n    //   block: 'center',\n    //   inline: 'center',\n    //   scrollMode: 'always',\n    //   boundary: container,\n    // })\n    // ```\n    //\n    // TO SWITCH TO THE LIBRARY:\n    // 1. Install: `yarn add scroll-into-view-if-needed`\n    // 2. Import: `import scrollIntoView from 'scroll-into-view-if-needed'`\n    // 3. Replace the custom code below with the library call above\n    //\n    // Reference source code available at:\n    // - reference/scroll-into-view-if-needed/\n    // - reference/compute-scroll-into-view/\n    // -------------------------------------------------------------------------\n\n    // Safety check: ensure elements are connected to the DOM\n    if (!container || !anchor || !shouldScroll || !anchor.isConnected) {\n      return\n    }\n\n    const containerRect = container.getBoundingClientRect()\n    const anchorRect = anchor.getBoundingClientRect()\n\n    // \"if-needed\" check: only scroll if anchor is outside the visible area\n    const isAbove = anchorRect.top < containerRect.top\n    const isBelow = anchorRect.bottom > containerRect.bottom\n\n    if (isAbove || isBelow) {\n      // Calculate scroll position to center the anchor in the container\n      // This mimics `block: 'center'` from the library\n      const anchorCenter = anchor.offsetTop - container.offsetTop + anchor.offsetHeight / 2\n      const containerCenter = container.clientHeight / 2\n      const scrollTop = anchorCenter - containerCenter\n\n      // Scroll only the container (boundary constraint), not the page\n      container.scrollTo({\n        top: Math.max(0, scrollTop),\n        behavior: \"smooth\",\n      })\n    }\n  }, [containerRef, shouldScroll])\n\n  useEffect(() => {\n    onActiveChange?.(isActive)\n  }, [isActive, onActiveChange])\n\n  return (\n    <a ref={mergeRefs(anchorRef, ref)} data-active={isActive} {...props}>\n      {props.children}\n    </a>\n  )\n}\n\n// ============================================================================\n// TOCItems - Simple TOC list with straight border line\n// ============================================================================\n\nexport interface TOCItemsProps extends ComponentProps<\"div\"> {\n  /**\n   * Text to display when there are no headings\n   * @defaultValue \"No Headings\"\n   */\n  emptyText?: string\n}\n\nexport function TOCItems({ ref, className, emptyText = \"No Headings\", ...props }: TOCItemsProps) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const items = useTOCItems()\n\n  if (items.length === 0) {\n    return <div className=\"rounded-lg border bg-card p-3 text-xs text-muted-foreground\">{emptyText}</div>\n  }\n\n  return (\n    <>\n      <TocThumb\n        containerRef={containerRef}\n        className=\"absolute top-[var(--toc-top)] h-[var(--toc-height)] w-px bg-primary transition-all\"\n      />\n      <div\n        ref={mergeRefs(ref, containerRef)}\n        className={cn(\"flex flex-col border-s border-foreground/10\", className)}\n        {...props}\n      >\n        {items.map(item => (\n          <SimpleTOCItem key={item.url} item={item} />\n        ))}\n      </div>\n    </>\n  )\n}\n\nfunction SimpleTOCItem({ item }: { item: TOCItemType }) {\n  return (\n    <TOCItem\n      href={item.url}\n      className={cn(\n        \"prose py-1.5 text-sm text-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-primary hover:text-accent-foreground\",\n        item.depth <= 2 && \"ps-3\",\n        item.depth === 3 && \"ps-6\",\n        item.depth >= 4 && \"ps-8\",\n      )}\n    >\n      {item.title}\n    </TOCItem>\n  )\n}\n\n// ============================================================================\n// ClerkTOCItems - TOC with depth-aware line that follows the hierarchy\n// ============================================================================\n\nfunction getItemOffset(depth: number): number {\n  if (depth <= 2) return 14\n  if (depth === 3) return 26\n  return 36\n}\n\nfunction getLineOffset(depth: number): number {\n  return depth >= 3 ? 10 : 0\n}\n\nexport interface ClerkTOCItemsProps extends ComponentProps<\"div\"> {\n  /**\n   * Text to display when there are no headings\n   * @defaultValue \"No Headings\"\n   */\n  emptyText?: string\n}\n\nexport function ClerkTOCItems({ ref, className, emptyText = \"No Headings\", ...props }: ClerkTOCItemsProps) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const items = useTOCItems()\n\n  const [svg, setSvg] = useState<{\n    path: string\n    width: number\n    height: number\n  }>()\n\n  useEffect(() => {\n    if (!containerRef.current) return\n    const container = containerRef.current\n\n    function onResize(): void {\n      if (container.clientHeight === 0) return\n      let w = 0\n      let h = 0\n      const d: string[] = []\n\n      for (let i = 0; i < items.length; i++) {\n        const element: HTMLElement | null = container.querySelector(`a[href=\"#${items[i].url.slice(1)}\"]`)\n        if (!element) continue\n\n        const styles = getComputedStyle(element)\n        const offset = getLineOffset(items[i].depth) + 1\n        const top = element.offsetTop + parseFloat(styles.paddingTop)\n        const bottom = element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom)\n\n        w = Math.max(offset, w)\n        h = Math.max(h, bottom)\n\n        d.push(`${i === 0 ? \"M\" : \"L\"}${offset} ${top}`)\n        d.push(`L${offset} ${bottom}`)\n      }\n\n      setSvg({\n        path: d.join(\" \"),\n        width: w + 1,\n        height: h,\n      })\n    }\n\n    const observer = new ResizeObserver(onResize)\n    onResize()\n\n    observer.observe(container)\n    return () => {\n      observer.disconnect()\n    }\n  }, [items])\n\n  if (items.length === 0) {\n    return <div className=\"rounded-lg border bg-card p-3 text-xs text-muted-foreground\">{emptyText}</div>\n  }\n\n  return (\n    <>\n      {svg ? (\n        <div\n          className=\"absolute start-0 top-0 rtl:-scale-x-100\"\n          style={{\n            width: svg.width,\n            height: svg.height,\n            maskImage: `url(\"data:image/svg+xml,${encodeURIComponent(\n              `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${svg.width} ${svg.height}\"><path d=\"${svg.path}\" stroke=\"black\" stroke-width=\"1\" fill=\"none\" /></svg>`,\n            )}\")`,\n          }}\n        >\n          <TocThumb\n            containerRef={containerRef}\n            className=\"mt-[var(--toc-top)] h-[var(--toc-height)] bg-primary transition-all\"\n          />\n        </div>\n      ) : null}\n      <div ref={mergeRefs(containerRef, ref)} className={cn(\"flex flex-col\", className)} {...props}>\n        {items.map((item, i) => (\n          <ClerkTOCItemElement key={item.url} item={item} upper={items[i - 1]?.depth} lower={items[i + 1]?.depth} />\n        ))}\n      </div>\n    </>\n  )\n}\n\nfunction ClerkTOCItemElement({\n  item,\n  upper = item.depth,\n  lower = item.depth,\n}: {\n  item: TOCItemType\n  upper?: number\n  lower?: number\n}) {\n  const offset = getLineOffset(item.depth)\n  const upperOffset = getLineOffset(upper)\n  const lowerOffset = getLineOffset(lower)\n\n  return (\n    <TOCItem\n      href={item.url}\n      style={{\n        paddingInlineStart: getItemOffset(item.depth),\n      }}\n      className=\"prose relative py-1.5 text-sm text-muted-foreground hover:text-accent-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-primary\"\n    >\n      {offset !== upperOffset ? (\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 16 16\"\n          className=\"absolute -top-1.5 start-0 size-4 rtl:-scale-x-100\"\n        >\n          <line x1={upperOffset} y1=\"0\" x2={offset} y2=\"12\" className=\"stroke-foreground/10\" strokeWidth=\"1\" />\n        </svg>\n      ) : null}\n      <div\n        className={cn(\n          \"absolute inset-y-0 w-px bg-foreground/10\",\n          offset !== upperOffset && \"top-1.5\",\n          offset !== lowerOffset && \"bottom-1.5\",\n        )}\n        style={{\n          insetInlineStart: offset,\n        }}\n      />\n      {item.title}\n    </TOCItem>\n  )\n}\n\n// ============================================================================\n// PageTOC & PageTOCItems - Convenience wrappers for page layout\n// ============================================================================\n\nexport interface PageTOCProps extends ComponentProps<\"div\"> {\n  children?: ReactNode\n}\n\nexport function PageTOC({ className, children, ...props }: PageTOCProps) {\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)} {...props}>\n      {children}\n    </div>\n  )\n}\n\nexport interface PageTOCItemsProps extends ComponentProps<\"div\"> {\n  /**\n   * TOC variant style\n   * - \"default\": Simple straight border line\n   * - \"clerk\": Depth-aware line that follows hierarchy\n   * @defaultValue \"default\"\n   */\n  variant?: \"default\" | \"clerk\"\n  /**\n   * Text to display when there are no headings\n   * @defaultValue \"No Headings\"\n   */\n  emptyText?: string\n}\n\nexport function PageTOCItems({ variant = \"default\", emptyText, ...props }: PageTOCItemsProps) {\n  return (\n    <TOCScrollArea>\n      {variant === \"clerk\" ? (\n        <ClerkTOCItems emptyText={emptyText} {...props} />\n      ) : (\n        <TOCItems emptyText={emptyText} {...props} />\n      )}\n    </TOCScrollArea>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}
