{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nav-link",
  "title": "Nav Link",
  "description": "Adds active, exact, and pending route state to Next.js App Router links, with typed routes, nested matching, and Cache Components support.",
  "files": [
    {
      "path": "registry/base/ui/nav-link.tsx",
      "content": "\"use client\";\n\nimport type { Route } from \"next\";\nimport Link, { useLinkStatus } from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport {\n  Suspense,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\n\nexport type NavLinkMatch = \"exact\" | \"prefix\";\n\nexport type NavLinkActiveProps = {\n  isActive: boolean;\n  isExact: boolean;\n};\n\nexport type NavLinkRenderProps = NavLinkActiveProps & {\n  isPending: boolean;\n};\n\nexport type NavLinkProps<T extends string = string> = Omit<\n  ComponentProps<typeof Link>,\n  \"aria-current\" | \"children\" | \"className\" | \"href\"\n> & {\n  /**\n   * A typed Next.js route or an absolute URL. When `typedRoutes` is enabled,\n   * invalid literal routes are reported by TypeScript.\n   */\n  href: Route<T> | URL;\n  /**\n   * Defaults to `exact`, so a link is active only on its own page. Opt into\n   * `prefix` for a section link that should stay active on descendant routes.\n   * The root route always matches exactly.\n   */\n  match?: NavLinkMatch;\n  /**\n   * Overrides `usePathname()`. Useful for previews, tests, rewrites, and\n   * routing layers that expose a canonical pathname.\n   */\n  currentPathname?: string;\n  className?:\n    | string\n    | ((props: NavLinkActiveProps) => string | undefined);\n  children?: ReactNode | ((props: NavLinkRenderProps) => ReactNode);\n};\n\n/**\n * A Next.js App Router link with active, exact, and pending route state.\n *\n * `className` receives active route state. Function children additionally\n * receive `isPending` because `useLinkStatus()` must run inside the Link.\n */\nexport function NavLink<T extends string = string>({\n  currentPathname,\n  ...props\n}: NavLinkProps<T>) {\n  if (currentPathname !== undefined) {\n    return <ResolvedNavLink {...props} pathname={currentPathname} controlled />;\n  }\n\n  return (\n    <Suspense fallback={<NavLinkShell {...props} state={inactiveState} />}>\n      <PathnameNavLink {...props} />\n    </Suspense>\n  );\n}\n\ntype PathnameNavLinkProps<T extends string = string> = Omit<\n  NavLinkProps<T>,\n  \"currentPathname\"\n>;\n\nconst inactiveState: NavLinkActiveProps = {\n  isActive: false,\n  isExact: false,\n};\n\nfunction PathnameNavLink<T extends string>(props: PathnameNavLinkProps<T>) {\n  return <ResolvedNavLink {...props} pathname={usePathname()} />;\n}\n\ntype NavLinkShellProps<T extends string = string> = PathnameNavLinkProps<T> & {\n  state: NavLinkActiveProps;\n  /** Skips the first-paint script, which cannot know the controlled pathname. */\n  controlled?: boolean;\n};\n\nfunction ResolvedNavLink<T extends string>({\n  href,\n  match = \"exact\",\n  pathname,\n  ...props\n}: Omit<NavLinkShellProps<T>, \"state\"> & { pathname: string }) {\n  const targetPathname = getHrefPathname(href);\n  const isExact = targetPathname\n    ? isPathActive(pathname, targetPathname, \"exact\")\n    : false;\n  const isActive = targetPathname\n    ? isPathActive(pathname, targetPathname, match)\n    : false;\n\n  return (\n    <NavLinkShell\n      {...props}\n      href={href}\n      match={match}\n      state={{ isActive, isExact }}\n    />\n  );\n}\n\nfunction NavLinkShell<T extends string>({\n  href,\n  match = \"exact\",\n  state,\n  controlled,\n  className,\n  children,\n  ...props\n}: NavLinkShellProps<T>) {\n  const targetPathname = controlled ? undefined : getHrefPathname(href);\n\n  return (\n    <Link\n      {...props}\n      href={href}\n      aria-current={\n        state.isActive ? (state.isExact ? \"page\" : \"location\") : undefined\n      }\n      data-slot=\"nav-link\"\n      data-active={state.isActive ? \"\" : undefined}\n      data-navlink-href={targetPathname}\n      data-navlink-match={targetPathname ? match : undefined}\n      suppressHydrationWarning\n      className={typeof className === \"function\" ? className(state) : className}\n    >\n      {typeof children === \"function\" ? (\n        <NavLinkRenderChildren state={state} render={children} />\n      ) : (\n        children\n      )}\n    </Link>\n  );\n}\n\nfunction NavLinkRenderChildren({\n  state,\n  render,\n}: {\n  state: NavLinkActiveProps;\n  render: (props: NavLinkRenderProps) => ReactNode;\n}) {\n  const { pending } = useLinkStatus();\n\n  return <>{render({ ...state, isPending: pending })}</>;\n}\n\n/**\n * Optional first-paint enhancement for CSS that targets `data-active` or\n * `aria-current`. Render it once at the end of the root layout body.\n *\n * Pass `basePath` when the app sets one in `next.config`, because\n * `location.pathname` includes it while `href` does not. Links using\n * `currentPathname` are skipped, since only the app knows that value.\n */\nexport function NavLinkScript({\n  nonce,\n  basePath,\n}: {\n  nonce?: string;\n  basePath?: string;\n}) {\n  return (\n    <script\n      nonce={nonce}\n      data-slot=\"nav-link-script\"\n      type={typeof window === \"undefined\" ? \"text/javascript\" : \"text/plain\"}\n      suppressHydrationWarning\n      dangerouslySetInnerHTML={{ __html: getNavLinkScript(basePath) }}\n    />\n  );\n}\n\nexport function isPathActive(\n  pathname: string,\n  targetPathname: string,\n  match: NavLinkMatch = \"exact\",\n) {\n  const current = normalizePathname(pathname);\n  const target = normalizePathname(targetPathname);\n\n  if (match === \"exact\" || target === \"/\") {\n    return current === target;\n  }\n\n  return current === target || current.startsWith(`${target}/`);\n}\n\n/**\n * Returns a comparable pathname only for root-relative hrefs. Absolute URLs,\n * protocol-relative hrefs, relative segments, and bare `?`/`#` hrefs have no\n * route identity of their own and are never treated as active.\n */\nfunction getHrefPathname<T extends string>(href: Route<T> | URL) {\n  if (href instanceof URL || !href.startsWith(\"/\") || href.startsWith(\"//\")) {\n    return undefined;\n  }\n\n  return normalizePathname(href);\n}\n\nfunction normalizePathname(pathname: string) {\n  const pathOnly = pathname.split(/[?#]/, 1)[0] || \"/\";\n\n  if (pathOnly === \"/\") {\n    return pathOnly;\n  }\n\n  return pathOnly.replace(/\\/+$/, \"\") || \"/\";\n}\n\nfunction getNavLinkScript(basePath?: string) {\n  return `(function(){\n  function normalize(value) {\n    var path = (value || '/').split(/[?#]/, 1)[0] || '/';\n    return path === '/' ? path : path.replace(/\\\\/+$/, '') || '/';\n  }\n\n  var base = normalize(${JSON.stringify(basePath ?? \"\").replace(/</g, \"\\\\u003c\")});\n  var pathname = normalize(location.pathname);\n\n  if (base !== '/' && (pathname === base || pathname.indexOf(base + '/') === 0)) {\n    pathname = normalize(pathname.slice(base.length));\n  }\n\n  document.querySelectorAll('[data-navlink-href]').forEach(function(link) {\n    var target = normalize(link.getAttribute('data-navlink-href'));\n    var match = link.getAttribute('data-navlink-match') || 'exact';\n    var isExact = pathname === target;\n    var isActive = match === 'exact' || target === '/'\n      ? isExact\n      : isExact || pathname.startsWith(target + '/');\n\n    if (isActive) {\n      link.setAttribute('data-active', '');\n      link.setAttribute('aria-current', isExact ? 'page' : 'location');\n    } else {\n      link.removeAttribute('data-active');\n      link.removeAttribute('aria-current');\n    }\n  });\n})()`;\n}\n",
      "type": "registry:ui",
      "target": "@ui/nav-link.tsx"
    }
  ],
  "meta": {
    "tags": [
      "nextjs-only",
      "nextjs",
      "app-router",
      "active-link",
      "pending-state",
      "nested-routes",
      "render-props",
      "aria-current",
      "typed-routes",
      "cache-components"
    ],
    "effects": [
      "active-state",
      "pending-state"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}