{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toc-minimap",
  "title": "TOC Minimap",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Navigate page sections with a compact, hoverable TOC minimap.",
  "registryDependencies": [
    "hover-card",
    "@soundcn/u-mini-map-open"
  ],
  "files": [
    {
      "path": "src/registry/components/toc-minimap/toc-minimap.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useMemo, useState } from \"react\"\n\nimport { uMiniMapOpenSound } from \"@/lib/soundcn/u-mini-map-open\"\nimport { cn } from \"@/lib/utils\"\nimport { useSound } from \"@/hooks/soundcn/use-sound\"\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\"\n\nexport type TOCItemType = {\n  title: React.ReactNode\n  url: string\n  depth: number\n}\n\nexport type TOCMinimapProps = {\n  /** @fumadocsHref #tocitemtype */\n  items: TOCItemType[]\n  className?: string\n}\n\nexport function TOCMinimap({ items, className }: TOCMinimapProps) {\n  const itemIds = useMemo(\n    () => items.map((item) => item.url.replace(\"#\", \"\")),\n    [items]\n  )\n\n  const activeHeading = useActiveHeading(itemIds)\n\n  const [play] = useSound(uMiniMapOpenSound, { volume: 0.3 })\n\n  if (!items.length) {\n    return null\n  }\n\n  return (\n    <div className={cn(\"ml-auto w-18\", className)}>\n      <HoverCard\n        onOpenChange={(open) => {\n          if (open) play()\n        }}\n      >\n        <HoverCardTrigger\n          delay={0}\n          closeDelay={0}\n          render={\n            <div className=\"flex max-h-[50dvh] flex-col gap-3 overflow-hidden py-3 pl-6 opacity-100 transition-opacity duration-200 data-popup-open:opacity-0\">\n              {items.map((item) => (\n                <div\n                  key={item.url}\n                  data-depth={item.depth}\n                  data-active={item.url === `#${activeHeading}`}\n                  className={cn(\n                    \"h-0.5 w-6 shrink-0 rounded-xs bg-ring/50 transition-[background-color] duration-200\",\n                    \"data-[depth=3]:ml-2 data-[depth=3]:w-4\",\n                    \"data-[depth=4]:ml-4 data-[depth=4]:w-2\",\n                    \"data-active:bg-foreground\"\n                  )}\n                />\n              ))}\n            </div>\n          }\n        />\n\n        <HoverCardContent\n          className=\"w-56 overflow-hidden p-0 duration-200 data-[side=left]:slide-in-from-right-3 data-[side=left]:slide-out-to-right-3 data-open:zoom-in-100 data-closed:zoom-out-100\"\n          align=\"start\"\n          alignOffset={0}\n          side=\"left\"\n          sideOffset={-60}\n        >\n          <div className=\"flex max-h-[50dvh] overflow-y-auto overscroll-contain\">\n            <ul className=\"flex size-full flex-col px-6 py-4 text-sm\">\n              {items.map((item) => (\n                <li key={item.url} className=\"flex py-1\">\n                  <a\n                    href={item.url}\n                    data-depth={item.depth}\n                    data-active={item.url === `#${activeHeading}`}\n                    className={cn(\n                      \"line-clamp-2 w-full transition-[color] duration-200\",\n                      \"text-muted-foreground hover:text-foreground data-active:text-foreground\",\n                      \"data-[depth=3]:pl-4 data-[depth=4]:pl-8\"\n                    )}\n                    onClick={handleItemClick}\n                  >\n                    {item.title}\n                  </a>\n                </li>\n              ))}\n            </ul>\n          </div>\n        </HoverCardContent>\n      </HoverCard>\n    </div>\n  )\n}\n\nexport function useActiveHeading(itemIds: string[]) {\n  const [activeId, setActiveId] = useState<string | null>(null)\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            setActiveId(entry.target.id)\n          }\n        }\n      },\n      { rootMargin: \"0% 0% -80% 0%\", threshold: 0.98 }\n    )\n\n    for (const id of itemIds ?? []) {\n      const element = document.getElementById(id)\n      if (element) {\n        observer.observe(element)\n      }\n    }\n\n    return () => {\n      for (const id of itemIds ?? []) {\n        const element = document.getElementById(id)\n        if (element) {\n          observer.unobserve(element)\n        }\n      }\n    }\n  }, [itemIds])\n\n  return activeId\n}\n\nfunction handleItemClick(e: React.MouseEvent<HTMLAnchorElement>) {\n  e.preventDefault()\n  const url = e.currentTarget.getAttribute(\"href\") ?? \"\"\n  scrollToHeading(url)\n}\n\nfunction scrollToHeading(url: string) {\n  history.pushState(null, \"\", url)\n  document.getElementById(url.replace(\"#\", \"\"))?.scrollIntoView({\n    behavior: \"smooth\",\n  })\n}\n",
      "type": "registry:component",
      "target": "@components/toc-minimap.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/toc-minimap",
  "categories": [
    "navigation"
  ],
  "type": "registry:component"
}