{"_id":"@atlas-kit/toc-nav","_rev":"2-b5b21119ee7fd8dbfd70938336946b60","name":"@atlas-kit/toc-nav","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@atlas-kit/toc-nav","version":"0.1.0","keywords":["react","nextjs","table-of-contents","toc","scroll","navigation","svg"],"license":"MIT","_id":"@atlas-kit/toc-nav@0.1.0","maintainers":[{"name":"ww96","email":"juwolf11@gmail.com"}],"dist":{"shasum":"8c576af6d5d40cfb44f81a3fe78ed8bda8dbfed9","tarball":"https://registry.npmjs.org/@atlas-kit/toc-nav/-/toc-nav-0.1.0.tgz","fileCount":6,"integrity":"sha512-9ba8LJu6PKU4MB9IyzljYxWa5FitNlD9pEGrh9rRCRYab5Ht+y6QwKe2RC2UmSBIqnH2BfIGrBhqLrbpQRU5kw==","signatures":[{"sig":"MEUCIFAmv+JZqvdTFEjqp26ytzWHsJLDV1YZBV6qF5IvNrktAiEA7SpLhgz0THbTwTewAAMYYioW3tvFX43FHI/QGvcc2Gc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42340},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"247c953ac2145be300ed5a7ba317b2a695bb6c6e","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"ww96","email":"juwolf11@gmail.com"},"_npmVersion":"11.12.1","description":"Scroll-tracked animated Table of Contents for Next.js / React","directories":{},"_nodeVersion":"25.8.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8","typescript":"^5","@types/react":"^18","@types/react-dom":"^18"},"peerDependencies":{"react":">=18","react-dom":">=18"},"_npmOperationalInternal":{"tmp":"tmp/toc-nav_0.1.0_1775442555260_0.8165963277617136","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@atlas-kit/toc-nav","version":"0.1.1","description":"Scroll-tracked animated Table of Contents for Next.js / React","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./styles.css":"./styles.css"},"scripts":{"build":"tsup","dev":"tsup --watch"},"keywords":["react","nextjs","table-of-contents","toc","scroll","navigation","svg"],"peerDependencies":{"react":">=18","react-dom":">=18"},"devDependencies":{"@types/react":"^18","@types/react-dom":"^18","tsup":"^8","typescript":"^5"},"license":"MIT","gitHead":"35d2e18202d3eaa848a0565e475b888842433ca9","_id":"@atlas-kit/toc-nav@0.1.1","_nodeVersion":"25.8.2","_npmVersion":"11.12.1","dist":{"integrity":"sha512-k8r24PcxSlxvkgYahuTJ78CQRxAUDL56HrlsfmOTEXQIXWEuprIGjFEFJNFzgvercONn4B/CZZ5MfObk6HAiUw==","shasum":"4a48ebf9c8ab59b0f75216d733d8ab205f224be5","tarball":"https://registry.npmjs.org/@atlas-kit/toc-nav/-/toc-nav-0.1.1.tgz","fileCount":7,"unpackedSize":46756,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICKkIQClYVkerd85U74e29qQTAJW9IagUe6JzAOgzocGAiEAqoYNXx3np1tE9yvXGxN/CYOTz5W87HGQMwYbeXJu0nw="}]},"_npmUser":{"name":"ww96","email":"juwolf11@gmail.com"},"directories":{},"maintainers":[{"name":"ww96","email":"juwolf11@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/toc-nav_0.1.1_1775446254843_0.29018406657116125"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-06T02:29:15.191Z","modified":"2026-04-06T03:30:55.144Z","0.1.0":"2026-04-06T02:29:15.442Z","0.1.1":"2026-04-06T03:30:55.007Z"},"license":"MIT","keywords":["react","nextjs","table-of-contents","toc","scroll","navigation","svg"],"description":"Scroll-tracked animated Table of Contents for Next.js / React","maintainers":[{"name":"ww96","email":"juwolf11@gmail.com"}],"readme":"# toc-nav\n\nA scroll-synchronized navigation component with an animated SVG path and a dot that rides the track as you scroll.\n\n![toc-nav preview](./preview/preview.gif)\n\n---\n\n## Features\n\n- **Scroll-synced SVG path** not IntersectionObserver hacks; pixel-accurate mapping from document position to SVG coordinates\n- **Viewport-aware clip** the bright segment shows exactly what's in view, dim track shows the rest\n- **Dot that rides the path** binary-searched every rAF frame, zero React re-renders\n- **Rounded indent steps** smooth bezier corners when stepping between h2 and h3\n- **Three layout modes** `right` (fixed sidebar), `left`, or `inline` (flows with content)\n- **Headless core** `useTocNavigation` hook so you can build any UI on top\n- **No extra dependencies** no lucide-react, no clsx, no external cn utility\n\n---\n\n## Installation\n\n```bash\nnpm install toc-nav\n```\n\n```bash\n# optional: import default CSS variable definitions\nimport 'toc-nav/styles.css'\n```\n\n**Peer deps:** React 18+\n\n---\n\n## Quick start\n\n```tsx\nimport { TocNav } from 'toc-nav'\nimport type { TocItem } from 'toc-nav'\n\nconst headings: TocItem[] = [\n  { id: 'intro',  text: 'Introduction',  level: 2 },\n  { id: 'setup',  text: 'Setup',         level: 2 },\n  { id: 'config', text: 'Configuration', level: 3 },\n  { id: 'deploy', text: 'Deploy',        level: 2 },\n]\n\nexport default function DocsLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <TocNav items={headings} containerWidth=\"56rem\" />\n      <article>{children}</article>\n    </div>\n  )\n}\n```\n\nRenders a **collapsible mobile bar** and a **fixed desktop sidebar**. Disappears automatically when fewer than 2 headings are passed.\n\n---\n\n## Props\n\n### `TocNav`\n\n| Prop             | Type          | Default          | Description |\n| ---------------- | ------------- | ---------------- | ----------- |\n| `items`          | `TocItem[]`   | —                | Ordered heading list. Each item needs `id`, `text`, `level`. |\n| `position`       | `TocPosition` | `\"right\"`        | `\"right\"` fixed sidebar · `\"left\"` fixed sidebar · `\"inline\"` flows with the document |\n| `containerWidth` | `string`      | —                | Max-width of your content column (e.g. `\"56rem\"`). Used to compute the right offset so the TOC hugs the column edge. |\n| `headerOffset`   | `number`      | `88`             | Pixels reserved for a sticky header — affects scroll tracking and smooth-scroll target. |\n| `label`          | `string`      | `\"On this page\"` | Shown in the mobile toggle and as `aria-label` on the nav. |\n| `icon`           | `ReactNode`   | chevron SVG      | Custom icon for the mobile toggle button. |\n| `className`      | `string`      | —                | Extra classes merged onto the `<nav>` element. Use to override width, top offset, etc. |\n\n### `TocItem`\n\n```ts\ninterface TocItem {\n  id: string     // must match the heading element's id in the DOM\n  text: string\n  level: 2 | 3  // h2 and h3 only\n}\n```\n\n### `TocPosition`\n\n```ts\ntype TocPosition = 'right' | 'left' | 'inline'\n```\n\n| Value    | Behaviour |\n| -------- | --------- |\n| `right`  | Fixed to the right of the content column. Uses `containerWidth` for precise offset. Mobile shows a collapsible accordion. |\n| `left`   | Fixed to the left side of the viewport. Same mobile behaviour. |\n| `inline` | Flows with the document — visible at all breakpoints, no mobile toggle. Drop it between your intro and first section. |\n\n---\n\n## CSS variables\n\nThe SVG track reads these variables. Import `toc-nav/styles.css` to get safe defaults, or define your own:\n\n| Variable             | Default   | Used for                    |\n| -------------------- | --------- | --------------------------- |\n| `--color-primary`    | `#3b82f6` | Active track segment + dot  |\n| `--color-border`     | `#e2e8f0` | Dim background track        |\n| `--color-background` | `#ffffff` | Dot halo (punches the line) |\n\nAll variables have hardcoded fallbacks, so the component renders correctly even without the CSS import.\n\n---\n\n## Headless usage\n\nUse `useTocNavigation` when you want full control over markup and styling:\n\n```tsx\n'use client'\n\nimport { useTocNavigation } from 'toc-nav'\nimport type { TocItem } from 'toc-nav'\n\nexport function MyToc({ items }: { items: TocItem[] }) {\n  const {\n    visibleIds, svgPath, svgH, ready,\n    listRef, itemRefs,\n    clipRectRef, fgPathRef, dotGroupRef,\n    scrollTo,\n  } = useTocNavigation(items, { headerOffset: 64 })\n\n  return (\n    <nav style={{ opacity: ready ? 1 : 0 }}>\n      {/* your SVG + list markup here */}\n    </nav>\n  )\n}\n```\n\n### Hook return values\n\n| Key           | Type                                            | Description |\n| ------------- | ----------------------------------------------- | ----------- |\n| `visibleIds`  | `string[]`                                      | IDs of headings visible in (or nearest to) the viewport. |\n| `svgPath`     | `string`                                        | SVG `d` attribute for the track. Empty until after first layout. |\n| `svgH`        | `number`                                        | Height of the SVG canvas (equals the list's `scrollHeight`). |\n| `ready`       | `boolean`                                       | `true` after the first scroll computation — use to fade in. |\n| `listRef`     | `RefObject<HTMLUListElement>`                   | Attach to the `<ul>`. |\n| `itemRefs`    | `MutableRefObject<Map<string, HTMLLIElement>>`  | Attach each `<li>` by heading ID. |\n| `clipRectRef` | `RefObject<SVGRectElement>`                     | Attach to the clip `<rect>`. Updated every rAF frame. |\n| `fgPathRef`   | `RefObject<SVGPathElement>`                     | Attach to the foreground `<path>`. Used for dot positioning. |\n| `dotGroupRef` | `RefObject<SVGGElement>`                        | Attach to the dot `<g>`. Transform updated every rAF frame. |\n| `scrollTo`    | `(id: string) => void`                          | Smooth-scrolls to the heading anchor. |\n\n---\n\n## Extracting headings from MDX\n\n`TocItem` is framework-agnostic. Minimal extractor using `github-slugger` (same algorithm as `rehype-slug`):\n\n```ts\nimport GithubSlugger from 'github-slugger'\nimport type { TocItem } from 'toc-nav'\n\nexport function extractHeadings(mdxSource: string): TocItem[] {\n  const slugger = new GithubSlugger()\n  const matches = [...mdxSource.matchAll(/^(#{2,3})\\s+(.+)$/gm)]\n  return matches.map((m) => ({\n    level: m[1].length as 2 | 3,\n    text: m[2].trim().replace(/`([^`]+)`/g, '$1'),\n    id: slugger.slug(m[2].trim()),\n  }))\n}\n```\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}