{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "section-tabs",
  "title": "Section Tabs",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Sticky in-page tabs that follow the reader's section, slide an indicator to it, keep the active tab in view on narrow screens and scroll to a section on click.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/scroll.json",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/section-tabs.tsx",
      "content": "// Ballmac UI: Section Tabs. https://ui.ballmac.com/components/section-tabs\n\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { scrollToId, useScrollSpy, useScrolled, type ScrollContainer } from \"@/lib/ballmac/scroll\";\nimport { spring } from \"@/lib/ballmac/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype SectionTab = {\n  /** The id of the section in the page. */\n  id: string;\n  /** Tab text. */\n  label: string;\n};\n\ntype SectionTabsProps = Omit<React.ComponentProps<\"nav\">, \"children\"> & {\n  /** Sections in page order. */\n  sections: SectionTab[];\n  /** Accessible name of the navigation landmark. */\n  label?: string;\n  /** Pixels of space to keep above a section after scrolling to it (your sticky header plus this bar). */\n  offset?: number;\n  /** A scrollable element to watch and scroll instead of the page. */\n  container?: ScrollContainer;\n  /** Indicator style. */\n  variant?: \"underline\" | \"pill\";\n  /** Stick to the top of the page (or container) while scrolling. */\n  sticky?: boolean;\n  /** Distance from the top when sticky. */\n  stickyTop?: number;\n};\n\n/**\n * In-page navigation for long pages (pricing, product, docs). Tabs follow the reader: the current section is\n * highlighted with a sliding indicator, the active tab scrolls into view on narrow screens, and choosing a tab\n * scrolls to its section. Real links, so it works with the keyboard, new tabs and copied URLs.\n */\nfunction SectionTabs({\n  sections,\n  label,\n  offset = 72,\n  container,\n  variant = \"underline\",\n  sticky = true,\n  stickyTop = 0,\n  className,\n  style,\n  ...props\n}: SectionTabsProps) {\n  const msg = useMessages()\n  label ??= msg(\"section-tabs.label\", \"Sections\")\n  const reduce = useReducedMotion();\n  const indicatorId = `section-tabs-${React.useId()}`;\n  const ids = React.useMemo(() => sections.map((s) => s.id), [sections]);\n  const active = useScrollSpy(ids, { offset, container });\n  const stuck = useScrolled(offset, container);\n  const listRef = React.useRef<HTMLUListElement>(null);\n\n  React.useEffect(() => {\n    const list = listRef.current;\n    const el = list?.querySelector<HTMLElement>(\"[aria-current=location]\");\n    if (!list || !el) return;\n    const left = el.offsetLeft - (list.clientWidth - el.offsetWidth) / 2;\n    if (typeof list.scrollTo === \"function\") list.scrollTo({ left, behavior: reduce ? \"auto\" : \"smooth\" });\n  }, [active, reduce]);\n\n  return (\n    <nav\n      aria-label={label}\n      data-slot=\"section-tabs\"\n      data-stuck={stuck || undefined}\n      style={{ top: sticky ? stickyTop : undefined, ...style }}\n      className={cn(\n        \"z-30 border-b bg-background/85 backdrop-blur-md transition-shadow duration-200 data-[stuck]:shadow-[0_8px_24px_-16px_rgb(0_0_0/0.2)] motion-reduce:transition-none\",\n        sticky && \"sticky\",\n        className,\n      )}\n      {...props}\n    >\n      <ul\n        ref={listRef}\n        className=\"mx-auto flex max-w-5xl items-center gap-1 overflow-x-auto px-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n      >\n        {sections.map((section) => {\n          const isActive = section.id === active;\n          return (\n            <li key={section.id} className=\"shrink-0\">\n              <a\n                href={`#${section.id}`}\n                aria-current={isActive ? \"location\" : undefined}\n                onClick={(event) => {\n                  event.preventDefault();\n                  if (scrollToId(section.id, { offset: offset - 4, container })) history.replaceState(null, \"\", `#${section.id}`);\n                }}\n                className={cn(\n                  \"relative my-1.5 inline-flex h-9 items-center rounded-md px-3 text-sm font-medium whitespace-nowrap outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n                  isActive ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n                )}\n              >\n                {isActive && (\n                  <motion.span\n                    layoutId={indicatorId}\n                    aria-hidden=\"true\"\n                    transition={reduce ? { duration: 0 } : spring.snappy}\n                    className={cn(\n                      \"absolute\",\n                      variant === \"underline\"\n                        ? \"inset-x-2 -bottom-[7px] h-0.5 rounded-full bg-primary\"\n                        : \"inset-0 -z-10 rounded-md bg-accent\",\n                    )}\n                  />\n                )}\n                {section.label}\n              </a>\n            </li>\n          );\n        })}\n      </ul>\n    </nav>\n  );\n}\n\nexport { SectionTabs, type SectionTabsProps, type SectionTab };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/section-tabs.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "tabs",
      "scrollspy",
      "sticky",
      "anchor links"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "sections: {id,label} matching element ids in the page. offset is the space to leave for this bar and any header. variant underline or pill.",
      "whenToUse": [
        "Long product, pricing and docs pages",
        "Anchored sections of a settings page"
      ],
      "whenNotToUse": [
        "Switching views without scrolling; use tabs",
        "Vertical page outlines; use table-of-contents"
      ],
      "composesWith": [
        "table-of-contents",
        "navbar",
        "tabs"
      ],
      "a11y": [
        {
          "keys": "Enter",
          "action": "Scrolls to the section and updates the URL hash"
        },
        {
          "keys": "Tab",
          "action": "Real links; middle-click and copy link work"
        },
        {
          "keys": "Screen readers",
          "action": "Current section has aria-current=location"
        }
      ],
      "customization": [
        "variant",
        "sticky and stickyTop",
        "offset",
        "container"
      ]
    },
    "examples": [
      "section-tabs-demo",
      "section-tabs-states"
    ],
    "url": "https://ui.ballmac.com/components/section-tabs"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}