{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tab-nav",
  "title": "Tab Nav",
  "author": "@fma-ds/ds",
  "description": "Navigation between the pages of an area that looks like tabs: a `<nav>` of react-aria links where the current page carries `aria-current=\"page\"`.\n\nUnlike `Tabs` there are no panels — each item is a route — so use it for sub-pages and `Tabs` for in-page views.\n\n- **`variant`**: matches the tab styles — `default` (segmented on `muted`, the current page raised) or `line` (underlined).\n- **Current item**: when `isCurrent` is set or its `href` matches `TabNav`'s `currentHref` (exactly, or with `matchPrefix` also on sub-paths).\n- **`TabNavItem`**: takes an icon before its label; **`TabNavSeparator`** divides groups.\n- **Overflow**: when items don't fit, the strip scrolls sideways and keeps the current page in view.\n\nWrap the app in react-aria's `RouterProvider` for client routing.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "cn@^0.5.0",
    "react-aria-components@^1.22.1"
  ],
  "files": [
    {
      "path": "src/core/tab-nav/tab-nav.tsx",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport { Link, type LinkProps } from \"react-aria-components\";\n\ntype TabNavVariant = \"default\" | \"line\";\n\nconst TabNavContext = React.createContext<{ variant: TabNavVariant; currentHref?: string }>({\n  variant: \"default\",\n});\n\nconst tabNavListVariants = cva(\"inline-flex w-max items-center text-muted-foreground\", {\n  variants: {\n    variant: {\n      default: \"h-8 rounded-lg bg-muted p-[3px]\",\n      line: \"h-9 gap-1\",\n    },\n  },\n  defaultVariants: { variant: \"default\" },\n});\n\n/**\n * Route navigation that looks like tabs: a `<nav>` of links where the current\n * page carries `aria-current=\"page\"`. Unlike `Tabs`, there are no panels — each\n * item is a page. Overflowing items scroll sideways, keeping the current one\n * in view. Wrap the app in react-aria's `RouterProvider` for client routing.\n */\nfunction TabNav({\n  className,\n  listClassName,\n  variant = \"default\",\n  currentHref,\n  children,\n  ...props\n}: React.ComponentProps<\"nav\"> &\n  VariantProps<typeof tabNavListVariants> & {\n    /** The current path; an item whose `href` equals it is the current page. */\n    currentHref?: string;\n    listClassName?: string;\n  }) {\n  const scrollRef = React.useRef<HTMLDivElement>(null);\n  const resolvedVariant = variant ?? \"default\";\n  const value = React.useMemo(\n    () => ({ variant: resolvedVariant, currentHref }),\n    [resolvedVariant, currentHref],\n  );\n\n  // Bring the current item into view inside the strip, without scrolling the\n  // page; again whenever the strip resizes (e.g. once web fonts load).\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-run when the current page changes\n  React.useEffect(() => {\n    const scroller = scrollRef.current;\n    if (!scroller) return;\n    const reveal = () => {\n      const current = scroller.querySelector<HTMLElement>(\"[aria-current=page]\");\n      if (!current) return;\n      const view = scroller.getBoundingClientRect();\n      const box = current.getBoundingClientRect();\n      if (box.left < view.left) scroller.scrollLeft -= view.left - box.left;\n      else if (box.right > view.right) scroller.scrollLeft += box.right - view.right;\n    };\n    reveal();\n    const observer = new ResizeObserver(reveal);\n    for (const element of [scroller, scroller.firstElementChild]) {\n      if (element) observer.observe(element);\n    }\n    return () => observer.disconnect();\n  }, [currentHref]);\n\n  return (\n    <TabNavContext.Provider value={value}>\n      <nav\n        data-slot=\"tab-nav\"\n        data-variant={resolvedVariant}\n        className={cn(\"group/tab-nav min-w-0\", className)}\n        {...props}\n      >\n        <div\n          ref={scrollRef}\n          className=\"relative max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n        >\n          <ul\n            data-slot=\"tab-nav-list\"\n            className={cn(tabNavListVariants({ variant: resolvedVariant }), listClassName)}\n          >\n            {children}\n          </ul>\n        </div>\n      </nav>\n    </TabNavContext.Provider>\n  );\n}\n\n/**\n * One page. It's current when `isCurrent` is set, or when its `href` matches\n * the `TabNav`'s `currentHref` (exactly, or as a prefix with `matchPrefix`).\n */\nfunction TabNavItem({\n  className,\n  href,\n  isCurrent,\n  matchPrefix = false,\n  ...props\n}: Omit<LinkProps, \"className\"> & {\n  className?: string;\n  isCurrent?: boolean;\n  /** Also current on sub-paths of `href`. */\n  matchPrefix?: boolean;\n}) {\n  const { currentHref } = React.useContext(TabNavContext);\n  const current =\n    isCurrent ??\n    (!!href &&\n      !!currentHref &&\n      (currentHref === href || (matchPrefix && currentHref.startsWith(`${href}/`))));\n\n  return (\n    <li data-slot=\"tab-nav-item\" className=\"flex h-full\">\n      <Link\n        href={href}\n        aria-current={current ? \"page\" : undefined}\n        className={cn(\n          \"relative inline-flex h-full items-center justify-center gap-1.5 rounded-md border border-transparent px-2.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all outline-none hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n          // default: the current page is a raised segment\n          \"group-data-[variant=default]/tab-nav:aria-[current=page]:bg-background group-data-[variant=default]/tab-nav:aria-[current=page]:shadow-sm dark:group-data-[variant=default]/tab-nav:aria-[current=page]:border-input dark:group-data-[variant=default]/tab-nav:aria-[current=page]:bg-input\",\n          // line: the current page is underlined\n          \"after:absolute after:inset-x-1 after:bottom-0 after:h-0.5 after:rounded-full after:bg-foreground after:opacity-0 after:transition-opacity group-data-[variant=line]/tab-nav:aria-[current=page]:after:opacity-100\",\n          \"aria-[current=page]:text-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    </li>\n  );\n}\n\n/** A divider between groups of items. */\nfunction TabNavSeparator({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      data-slot=\"tab-nav-separator\"\n      className={cn(\"mx-1 h-4 w-px shrink-0 bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { TabNav, TabNavItem, TabNavSeparator, tabNavListVariants };\n",
      "type": "registry:component",
      "target": "components/fma-ds/tab-nav.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "license": "MIT",
    "tags": [
      "navigation",
      "tabs",
      "routing",
      "react-aria"
    ],
    "contentHash": "52832920fc1b"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}