{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-of-contents",
  "type": "registry:block",
  "title": "Table of Contents",
  "author": "toc-cn (https://github.com/lenxism/tocn)",
  "description": "In-page documentation table of contents with scroll-position spy, animated SVG tree indicator, and mobile sticky collapsible.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "collapsible",
    "item",
    "scroll-area",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/table-of-contents/table-of-contents.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ListTree } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { ItemGroup } from \"@/components/ui/item\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { type TableOfContentsItem as TocItemData } from \"@/hooks/use-table-of-contents\"\nimport {\n  buildTocPath,\n  getDepthOffset,\n  getDepthPadding,\n  measureLinkMetrics,\n  type TocLinkMetrics,\n} from \"@/lib/toc-path\"\nimport { cn } from \"@/lib/utils\"\n\ntype TableOfContentsContextValue = {\n  items: TocItemData[]\n  activeId?: string\n  activeIndex: number\n  onItemClick?: (id: string) => void\n}\n\nconst TableOfContentsContext =\n  React.createContext<TableOfContentsContextValue | null>(null)\n\nconst tableOfContentsStyles = {\n  shell: \"relative pl-1\",\n  header:\n    \"mb-3 flex items-center gap-2 font-mono text-[0.6875rem] leading-4 tracking-wide text-muted-foreground uppercase\",\n  icon: \"size-3.5 shrink-0\",\n  separator: \"mb-3 opacity-30\",\n  track: \"text-muted-foreground/25\",\n  activeTrack: \"text-foreground\",\n  item:\n    \"relative z-[1] block w-fit scroll-m-4 rounded-sm py-1 pr-2 text-[0.8125rem] leading-5 transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n  itemActive: \"text-foreground\",\n  itemTrail: \"text-foreground\",\n  itemIdle: \"text-muted-foreground/60 hover:text-muted-foreground\",\n  mobileHeader: \"border-b bg-background/80 backdrop-blur-sm transition-colors\",\n  mobileHeaderOpen: \"shadow-lg\",\n  mobileButton:\n    \"flex h-11 w-full items-center gap-2.5 px-4 py-2.5 text-start text-sm font-normal text-muted-foreground [&_svg]:size-4\",\n  mobileScrollArea: \"max-h-[50vh] px-4 pb-4\",\n  mobileList: \"pt-2 [&_a]:w-full [&_a]:py-2\",\n}\n\nfunction useTableOfContentsContext() {\n  const context = React.useContext(TableOfContentsContext)\n  if (!context) {\n    throw new Error(\n      \"TableOfContents components must be used within TableOfContents\"\n    )\n  }\n  return context\n}\n\ntype TableOfContentsProps = React.ComponentProps<\"nav\"> & {\n  items: TocItemData[]\n  activeId?: string\n  onItemClick?: (id: string) => void\n}\n\nfunction TableOfContents({\n  items,\n  activeId,\n  onItemClick,\n  className,\n  children,\n  ...props\n}: TableOfContentsProps) {\n  const activeIndex = activeId\n    ? items.findIndex((item) => item.id === activeId)\n    : -1\n\n  const contextValue = React.useMemo(\n    () => ({\n      items,\n      activeId,\n      activeIndex,\n      onItemClick,\n    }),\n    [items, activeId, activeIndex, onItemClick]\n  )\n\n  return (\n    <TableOfContentsContext.Provider value={contextValue}>\n      <nav\n        aria-label=\"Table of contents\"\n        className={cn(\"relative w-full\", className)}\n        {...props}\n      >\n        {children ?? (\n          <TableOfContentsContent />\n        )}\n      </nav>\n    </TableOfContentsContext.Provider>\n  )\n}\n\nfunction TableOfContentsContent() {\n  return (\n    <div className={tableOfContentsStyles.shell}>\n      <TableOfContentsHeader />\n      <Separator className={tableOfContentsStyles.separator} />\n      <TableOfContentsList />\n    </div>\n  )\n}\n\ntype TableOfContentsHeaderProps = React.ComponentProps<\"div\"> & {\n  title?: string\n}\n\nfunction TableOfContentsHeader({\n  title = \"On this page\",\n  className,\n  children,\n  ...props\n}: TableOfContentsHeaderProps) {\n  return (\n    <div\n      className={cn(\n        tableOfContentsStyles.header,\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <TableOfContentsIcon className={tableOfContentsStyles.icon} />\n          <span>{title}</span>\n        </>\n      )}\n    </div>\n  )\n}\n\nfunction TableOfContentsIcon({ className }: { className?: string }) {\n  return <ListTree aria-hidden=\"true\" className={className} />\n}\n\ntype IndicatorState = {\n  path: string\n  metrics: TocLinkMetrics[]\n  width: number\n  height: number\n}\n\nfunction getTocAnchor(\n  container: HTMLElement,\n  id: string\n): HTMLAnchorElement | null {\n  return container.querySelector<HTMLAnchorElement>(\n    `[data-toc-id=\"${CSS.escape(id)}\"]`\n  )\n}\n\nfunction TableOfContentsList({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { items, activeIndex } = useTableOfContentsContext()\n  const fadeId = React.useId().replace(/:/g, \"\")\n  const fadeGradientId = `${fadeId}-toc-line-fade-gradient`\n  const trackMaskId = `${fadeId}-toc-track-fade`\n  const activeTrackMaskId = `${fadeId}-toc-active-track-fade`\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const clipRef = React.useRef<HTMLDivElement>(null)\n  const [indicator, setIndicator] = React.useState<IndicatorState | null>(null)\n\n  const measurePath = React.useCallback(() => {\n    const container = containerRef.current\n    if (!container || container.clientHeight === 0) {\n      return\n    }\n\n    const metrics: TocLinkMetrics[] = []\n    let width = 0\n    let height = 0\n\n    for (const item of items) {\n      const anchor = getTocAnchor(container, item.id)\n\n      if (!anchor) {\n        continue\n      }\n\n      const { top, bottom } = measureLinkMetrics(anchor)\n      const x = getDepthOffset(item.depth ?? 2) + 0.5\n\n      width = Math.max(x + 8, width)\n      height = Math.max(bottom, height)\n      metrics.push({ x, top, bottom })\n    }\n\n    setIndicator({\n      path: buildTocPath(metrics),\n      metrics,\n      width,\n      height,\n    })\n  }, [items])\n\n  const updateClip = React.useCallback(() => {\n    const container = containerRef.current\n    const clip = clipRef.current\n\n    if (!container || !clip || activeIndex < 0) {\n      return\n    }\n\n    let top = Number.POSITIVE_INFINITY\n    let bottom = 0\n\n    for (let index = 0; index <= activeIndex; index += 1) {\n      const anchor = getTocAnchor(container, items[index].id)\n\n      if (!anchor) {\n        continue\n      }\n\n      const metrics = measureLinkMetrics(anchor)\n      top = Math.min(top, metrics.top)\n      bottom = Math.max(bottom, metrics.bottom)\n    }\n\n    if (top === Number.POSITIVE_INFINITY) {\n      return\n    }\n\n    clip.style.setProperty(\"--toc-top\", `${top}px`)\n    clip.style.setProperty(\"--toc-height\", `${bottom - top}px`)\n  }, [activeIndex, items])\n\n  React.useLayoutEffect(() => {\n    measurePath()\n    updateClip()\n\n    const container = containerRef.current\n    if (!container) {\n      return\n    }\n\n    const resizeObserver = new ResizeObserver(() => {\n      measurePath()\n      updateClip()\n    })\n\n    resizeObserver.observe(container)\n\n    return () => resizeObserver.disconnect()\n  }, [measurePath, updateClip])\n\n  React.useLayoutEffect(() => {\n    updateClip()\n  }, [updateClip, activeIndex, indicator])\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative flex flex-col\", className)}\n      {...props}\n    >\n      {indicator?.path ? (\n        <>\n          <svg\n            aria-hidden=\"true\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox={`0 0 ${indicator.width} ${indicator.height}`}\n            className=\"pointer-events-none absolute top-0 left-0 overflow-visible\"\n            style={{ width: indicator.width, height: indicator.height }}\n          >\n            <defs>\n              <linearGradient\n                id={fadeGradientId}\n                x1=\"0\"\n                x2=\"0\"\n                y1=\"0\"\n                y2={indicator.height}\n                gradientUnits=\"userSpaceOnUse\"\n              >\n                <stop offset=\"0%\" stopColor=\"white\" stopOpacity=\"0\" />\n                <stop offset=\"10%\" stopColor=\"white\" stopOpacity=\"1\" />\n                <stop offset=\"90%\" stopColor=\"white\" stopOpacity=\"1\" />\n                <stop offset=\"100%\" stopColor=\"white\" stopOpacity=\"0\" />\n              </linearGradient>\n              <mask\n                id={trackMaskId}\n                maskUnits=\"userSpaceOnUse\"\n                x=\"0\"\n                y=\"0\"\n                width={indicator.width}\n                height={indicator.height}\n              >\n                <rect\n                  width={indicator.width}\n                  height={indicator.height}\n                  fill={`url(#${fadeGradientId})`}\n                />\n              </mask>\n            </defs>\n            <path\n              d={indicator.path}\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              mask={`url(#${trackMaskId})`}\n              className={tableOfContentsStyles.track}\n            />\n            {indicator.metrics.map((metric, index) => {\n              const y = (metric.top + metric.bottom) / 2\n              const isReached = activeIndex >= 0 && index <= activeIndex\n\n              return (\n                <circle\n                  key={`${metric.x}-${y}`}\n                  cx={metric.x}\n                  cy={y}\n                  r={isReached ? 2.5 : 1.75}\n                  className={cn(\n                    \"transition-[r,color,opacity] duration-300\",\n                    isReached\n                      ? \"fill-foreground text-foreground\"\n                      : \"fill-background stroke-current text-muted-foreground/35\"\n                  )}\n                  strokeWidth=\"1\"\n                />\n              )\n            })}\n          </svg>\n\n          <div\n            ref={clipRef}\n            className=\"pointer-events-none absolute top-0 left-0\"\n            style={{ width: indicator.width, height: indicator.height }}\n          >\n            <svg\n              aria-hidden=\"true\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox={`0 0 ${indicator.width} ${indicator.height}`}\n              className=\"absolute transition-[clip-path] duration-300 ease-out\"\n              style={{\n                width: indicator.width,\n                height: indicator.height,\n                clipPath:\n                  \"polygon(0 var(--toc-top), 100% var(--toc-top), 100% calc(var(--toc-top) + var(--toc-height)), 0 calc(var(--toc-top) + var(--toc-height)))\",\n              }}\n            >\n              <defs>\n                <linearGradient\n                  id={`${fadeGradientId}-active`}\n                  x1=\"0\"\n                  x2=\"0\"\n                  y1=\"0\"\n                  y2={indicator.height}\n                  gradientUnits=\"userSpaceOnUse\"\n                >\n                  <stop offset=\"0%\" stopColor=\"white\" stopOpacity=\"0\" />\n                  <stop offset=\"10%\" stopColor=\"white\" stopOpacity=\"1\" />\n                  <stop offset=\"90%\" stopColor=\"white\" stopOpacity=\"1\" />\n                  <stop offset=\"100%\" stopColor=\"white\" stopOpacity=\"0\" />\n                </linearGradient>\n                <mask\n                  id={activeTrackMaskId}\n                  maskUnits=\"userSpaceOnUse\"\n                  x=\"0\"\n                  y=\"0\"\n                  width={indicator.width}\n                  height={indicator.height}\n                >\n                  <rect\n                    width={indicator.width}\n                    height={indicator.height}\n                    fill={`url(#${fadeGradientId}-active)`}\n                  />\n                </mask>\n              </defs>\n              <path\n                d={indicator.path}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.5\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                mask={`url(#${activeTrackMaskId})`}\n                className={tableOfContentsStyles.activeTrack}\n              />\n              {indicator.metrics.map((metric, index) => {\n                if (activeIndex < 0 || index > activeIndex) {\n                  return null\n                }\n\n                const y = (metric.top + metric.bottom) / 2\n\n                return (\n                  <circle\n                    key={`${metric.x}-${y}-active`}\n                    cx={metric.x}\n                    cy={y}\n                    r=\"3\"\n                    className=\"fill-background stroke-current text-foreground\"\n                    strokeWidth=\"1.5\"\n                  />\n                )\n              })}\n            </svg>\n          </div>\n        </>\n      ) : null}\n\n      <ItemGroup>\n        {items.map((item, index) => (\n          <TableOfContentsItem\n            key={item.id}\n            item={item}\n            index={index}\n          />\n        ))}\n      </ItemGroup>\n    </div>\n  )\n}\n\ntype TableOfContentsItemProps = {\n  item: TocItemData\n  index: number\n  className?: string\n}\n\nfunction TableOfContentsItem({\n  item,\n  index,\n  className,\n}: TableOfContentsItemProps) {\n  const { activeId, activeIndex, onItemClick } = useTableOfContentsContext()\n  const depth = item.depth ?? 2\n  const isActive = item.id === activeId\n  const inTrail = activeIndex >= 0 && index <= activeIndex\n\n  const handleClick = React.useCallback(\n    (event: React.MouseEvent<HTMLAnchorElement>) => {\n      event.preventDefault()\n      onItemClick?.(item.id)\n\n      const element = document.getElementById(item.id)\n      element?.scrollIntoView({ behavior: \"smooth\", block: \"start\" })\n    },\n    [item.id, onItemClick]\n  )\n\n  return (\n    <a\n      role=\"listitem\"\n      href={`#${item.id}`}\n      onClick={handleClick}\n      data-toc-id={item.id}\n      data-active={isActive}\n      data-in-trail={inTrail}\n      style={{ paddingInlineStart: getDepthPadding(depth) }}\n      className={cn(\n        tableOfContentsStyles.item,\n        inTrail\n          ? tableOfContentsStyles.itemTrail\n          : tableOfContentsStyles.itemIdle,\n        isActive && tableOfContentsStyles.itemActive,\n        className\n      )}\n    >\n      {item.title}\n    </a>\n  )\n}\n\ntype TableOfContentsProgressProps = React.ComponentProps<\"svg\"> & {\n  value: number\n}\n\nfunction TableOfContentsProgress({\n  value,\n  className,\n  ...props\n}: TableOfContentsProgressProps) {\n  const strokeWidth = 2\n  const size = 16\n  const clamped = Math.min(1, Math.max(0, value))\n  const radius = (size - strokeWidth) / 2\n  const circumference = 2 * Math.PI * radius\n  const offset = circumference - clamped * circumference\n\n  return (\n    <svg\n      role=\"progressbar\"\n      viewBox={`0 0 ${size} ${size}`}\n      aria-valuenow={Math.round(clamped * 100)}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      className={cn(\"size-4 shrink-0\", className)}\n      {...props}\n    >\n      <circle\n        cx={size / 2}\n        cy={size / 2}\n        r={radius}\n        fill=\"none\"\n        strokeWidth={strokeWidth}\n        strokeDasharray=\"1 3\"\n        strokeLinecap=\"round\"\n        className=\"stroke-current/25\"\n      />\n      <circle\n        cx={size / 2}\n        cy={size / 2}\n        r={radius}\n        fill=\"none\"\n        strokeWidth={strokeWidth}\n        stroke=\"currentColor\"\n        strokeDasharray={circumference}\n        strokeDashoffset={offset}\n        strokeLinecap=\"round\"\n        transform={`rotate(-90 ${size / 2} ${size / 2})`}\n        className=\"transition-all duration-300\"\n      />\n    </svg>\n  )\n}\n\ntype TableOfContentsMobileProps = React.ComponentProps<\"div\"> & {\n  items: TocItemData[]\n  activeId?: string\n  onItemClick?: (id: string) => void\n  title?: string\n}\n\nfunction TableOfContentsMobile({\n  items,\n  activeId,\n  onItemClick,\n  title = \"On this page\",\n  className,\n  ...props\n}: TableOfContentsMobileProps) {\n  const [open, setOpen] = React.useState(false)\n  const mobileRef = React.useRef<HTMLDivElement>(null)\n\n  const activeIndex = activeId\n    ? items.findIndex((item) => item.id === activeId)\n    : -1\n  const activeItem = activeIndex >= 0 ? items[activeIndex] : undefined\n  const progress =\n    activeIndex >= 0 ? (activeIndex + 1) / Math.max(1, items.length) : 0\n  const showActiveTitle = activeIndex >= 0 && !open\n\n  const handleItemClick = React.useCallback(\n    (id: string) => {\n      onItemClick?.(id)\n      setOpen(false)\n    },\n    [onItemClick]\n  )\n\n  React.useEffect(() => {\n    if (!open) {\n      return\n    }\n\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target\n\n      if (\n        target instanceof HTMLElement &&\n        mobileRef.current &&\n        !mobileRef.current.contains(target)\n      ) {\n        setOpen(false)\n      }\n    }\n\n    window.addEventListener(\"click\", handleClickOutside)\n\n    return () => window.removeEventListener(\"click\", handleClickOutside)\n  }, [open])\n\n  const contextValue = React.useMemo(\n    () => ({\n      items,\n      activeId,\n      activeIndex,\n      onItemClick: handleItemClick,\n    }),\n    [items, activeId, activeIndex, handleItemClick]\n  )\n\n  return (\n    <TableOfContentsContext.Provider value={contextValue}>\n      <Collapsible\n        ref={mobileRef}\n        open={open}\n        onOpenChange={setOpen}\n        data-toc-mobile=\"\"\n        className={cn(\"sticky top-0 z-10 lg:hidden\", className)}\n        {...props}\n      >\n        <TableOfContentsMobileContent\n          activeItemTitle={activeItem?.title}\n          open={open}\n          progress={progress}\n          showActiveTitle={showActiveTitle}\n          title={title}\n        />\n      </Collapsible>\n    </TableOfContentsContext.Provider>\n  )\n}\n\ntype TableOfContentsMobileContentProps = {\n  activeItemTitle?: string\n  open: boolean\n  progress: number\n  showActiveTitle: boolean\n  title: string\n}\n\nfunction TableOfContentsMobileContent({\n  activeItemTitle,\n  open,\n  progress,\n  showActiveTitle,\n  title,\n}: TableOfContentsMobileContentProps) {\n  return (\n    <header\n      className={cn(\n        tableOfContentsStyles.mobileHeader,\n        open && tableOfContentsStyles.mobileHeaderOpen\n      )}\n    >\n      <CollapsibleTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          aria-label={\n            activeItemTitle && !open\n              ? `${title}: ${activeItemTitle}`\n              : title\n          }\n          className={tableOfContentsStyles.mobileButton}\n        >\n          <TableOfContentsProgress\n            value={progress}\n            className={open ? \"text-foreground\" : undefined}\n          />\n          <span className=\"grid flex-1 *:col-start-1 *:row-start-1 *:my-auto\">\n            <span\n              className={cn(\n                \"truncate transition-[opacity,translate,color]\",\n                open && \"text-foreground\",\n                showActiveTitle &&\n                  \"pointer-events-none -translate-y-full opacity-0\"\n              )}\n              aria-hidden={showActiveTitle}\n            >\n              {title}\n            </span>\n            <span\n              className={cn(\n                \"truncate transition-[opacity,translate]\",\n                !showActiveTitle &&\n                  \"pointer-events-none translate-y-full opacity-0\"\n              )}\n              aria-hidden={!showActiveTitle}\n            >\n              {activeItemTitle}\n            </span>\n          </span>\n          <ChevronDown\n            className={cn(\n              \"mx-0.5 shrink-0 transition-transform\",\n              open && \"rotate-180\"\n            )}\n          />\n        </Button>\n      </CollapsibleTrigger>\n      <CollapsibleContent data-toc-mobile-content=\"\">\n        <div>\n          <ScrollArea className={tableOfContentsStyles.mobileScrollArea}>\n            <TableOfContentsList\n              className={tableOfContentsStyles.mobileList}\n            />\n          </ScrollArea>\n        </div>\n      </CollapsibleContent>\n    </header>\n  )\n}\n\nexport {\n  TableOfContents,\n  TableOfContentsHeader,\n  TableOfContentsIcon,\n  TableOfContentsItem,\n  TableOfContentsList,\n  TableOfContentsMobile,\n  TableOfContentsProgress,\n}\n\nexport type { TableOfContentsItem as TocItem } from \"@/hooks/use-table-of-contents\"\n\nexport { useScrollSpy } from \"@/hooks/use-table-of-contents\"\n",
      "type": "registry:component",
      "target": "components/table-of-contents.tsx"
    },
    {
      "path": "hooks/use-table-of-contents.ts",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\n\r\nexport type TableOfContentsItem = {\r\n  id: string\r\n  title: string\r\n  depth?: number\r\n}\r\n\r\nexport type UseScrollSpyOptions = {\r\n  root?: Element | null\r\n  /** Top offset in px. Headings above this line count as passed. */\r\n  rootMargin?: string\r\n}\r\n\r\nfunction parseTopOffset(rootMargin: string): number {\r\n  const top = rootMargin.trim().split(/\\s+/)[0] ?? \"0px\"\r\n  const value = Number.parseFloat(top)\r\n\r\n  if (Number.isNaN(value)) {\r\n    return 0\r\n  }\r\n\r\n  return Math.abs(value)\r\n}\r\n\r\nfunction getActiveIdFromScrollPosition(\r\n  ids: string[],\r\n  root: Element | null,\r\n  offset: number\r\n): string | undefined {\r\n  if (ids.length === 0) {\r\n    return undefined\r\n  }\r\n\r\n  const rootTop = root?.getBoundingClientRect().top ?? 0\r\n  let activeId: string | undefined = ids[0]\r\n\r\n  for (const id of ids) {\r\n    const element = document.getElementById(id)\r\n    if (!element) {\r\n      continue\r\n    }\r\n\r\n    const top = element.getBoundingClientRect().top - rootTop\r\n\r\n    if (top <= offset) {\r\n      activeId = id\r\n    }\r\n  }\r\n\r\n  return activeId\r\n}\r\n\r\nexport function useScrollSpy(\r\n  ids: string[],\r\n  options: UseScrollSpyOptions = {}\r\n): string | undefined {\r\n  const { root = null, rootMargin = \"0px 0px -80% 0px\" } = options\r\n  const [activeId, setActiveId] = React.useState<string | undefined>(ids[0])\r\n  const offset = parseTopOffset(rootMargin)\r\n\r\n  React.useEffect(() => {\r\n    if (ids.length === 0) {\r\n      setActiveId(undefined)\r\n      return\r\n    }\r\n\r\n    let frame = 0\r\n\r\n    const update = () => {\r\n      const nextActiveId = getActiveIdFromScrollPosition(ids, root, offset)\r\n\r\n      if (nextActiveId) {\r\n        setActiveId(nextActiveId)\r\n      }\r\n    }\r\n\r\n    const scheduleUpdate = () => {\r\n      cancelAnimationFrame(frame)\r\n      frame = requestAnimationFrame(update)\r\n    }\r\n\r\n    update()\r\n\r\n    const scrollTarget = root ?? window\r\n    scrollTarget.addEventListener(\"scroll\", scheduleUpdate, { passive: true })\r\n    window.addEventListener(\"resize\", scheduleUpdate, { passive: true })\r\n\r\n    return () => {\r\n      cancelAnimationFrame(frame)\r\n      scrollTarget.removeEventListener(\"scroll\", scheduleUpdate)\r\n      window.removeEventListener(\"resize\", scheduleUpdate)\r\n    }\r\n  }, [ids, root, offset])\r\n\r\n  return activeId\r\n}\r\n",
      "type": "registry:hook",
      "target": "hooks/use-table-of-contents.ts"
    },
    {
      "path": "lib/toc-path.ts",
      "content": "export type TocLinkMetrics = {\r\n  x: number\r\n  top: number\r\n  bottom: number\r\n}\r\n\r\nexport function normalizeDepth(depth: number): number {\r\n  if (depth <= 1) {\r\n    return depth + 2\r\n  }\r\n\r\n  return depth\r\n}\r\n\r\nexport function getDepthOffset(depth: number): number {\r\n  const level = normalizeDepth(depth)\r\n\r\n  if (level <= 2) {\r\n    return 8\r\n  }\r\n\r\n  if (level === 3) {\r\n    return 16\r\n  }\r\n\r\n  return 24\r\n}\r\n\r\nexport function getDepthPadding(depth: number): number {\r\n  const level = normalizeDepth(depth)\r\n\r\n  if (level <= 2) {\r\n    return 20\r\n  }\r\n\r\n  if (level === 3) {\r\n    return 32\r\n  }\r\n\r\n  return 44\r\n}\r\n\r\nexport function measureLinkMetrics(anchor: HTMLAnchorElement): {\r\n  top: number\r\n  bottom: number\r\n} {\r\n  const styles = getComputedStyle(anchor)\r\n\r\n  return {\r\n    top: anchor.offsetTop + Number.parseFloat(styles.paddingTop),\r\n    bottom:\r\n      anchor.offsetTop +\r\n      anchor.clientHeight -\r\n      Number.parseFloat(styles.paddingBottom),\r\n  }\r\n}\r\n\r\nexport function buildTocPath(metrics: TocLinkMetrics[]): string {\r\n  if (metrics.length === 0) {\r\n    return \"\"\r\n  }\r\n\r\n  let path = \"\"\r\n  let previousX = 0\r\n  let previousBottom = 0\r\n\r\n  for (let index = 0; index < metrics.length; index += 1) {\r\n    const { x, top, bottom } = metrics[index]\r\n\r\n    if (index === 0) {\r\n      path += `M ${x} ${top} L ${x} ${bottom}`\r\n    } else {\r\n      path += ` C ${previousX} ${top - 4} ${x} ${previousBottom + 4} ${x} ${top} L ${x} ${bottom}`\r\n    }\r\n\r\n    previousX = x\r\n    previousBottom = bottom\r\n  }\r\n\r\n  return path\r\n}\r\n",
      "type": "registry:lib",
      "target": "lib/toc-path.ts"
    }
  ]
}