{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "app-navigation", "title": "App Navigation", "description": "Router-neutral application navigation with composable header and sidebar variants.", "registryDependencies": ["askyourpolicy/ss-registry/stitch-base"], "files": [ { "path": "src/components/ui/app-navigation.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype AppNavigationVariant = \"header\" | \"sidebar\";\n\nconst AppNavigationContext = React.createContext(null);\n\ntype AppNavigationProps = React.ComponentProps<\"nav\"> & {\n variant?: AppNavigationVariant;\n};\n\nfunction AppNavigation({\n \"aria-label\": ariaLabel = \"Primary navigation\",\n children,\n className,\n variant = \"header\",\n ...props\n}: AppNavigationProps) {\n return (\n \n \n {children}\n \n \n );\n}\n\ntype AppNavigationLinkProps = useRender.ComponentProps<\"a\"> & {\n active?: boolean;\n icon?: React.ReactNode;\n trailing?: React.ReactNode;\n};\n\nfunction AppNavigationLink({\n active = false,\n children,\n className,\n icon,\n render,\n trailing,\n ...props\n}: AppNavigationLinkProps) {\n const variant = React.useContext(AppNavigationContext);\n if (!variant) {\n throw new Error(\"AppNavigationLink must be used inside AppNavigation.\");\n }\n\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(\n {\n \"aria-current\": active ? \"page\" : undefined,\n children: (\n <>\n {icon != null ? (\n \n {icon}\n \n ) : null}\n \n {children}\n \n {trailing != null ? (\n \n {trailing}\n \n ) : null}\n \n ),\n className: cn(\n \"outline-none transition-colors focus-visible:ring-3 focus-visible:ring-ring/50\",\n variant === \"header\"\n ? \"flex h-(--app-header-height) items-center gap-1.5 border-b-2 border-transparent px-2 text-sm leading-5 font-medium text-muted-foreground hover:text-foreground focus-visible:border-ring\"\n : \"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n active &&\n (variant === \"header\"\n ? \"border-foreground text-foreground\"\n : \"bg-sidebar-accent text-sidebar-accent-foreground\"),\n className,\n ),\n },\n props,\n ),\n render,\n state: {\n active,\n slot: \"app-navigation-link\",\n variant,\n },\n });\n}\n\nfunction isNavigationPathActive(pathname: string, routes: readonly string[]) {\n return routes.some((route) => {\n const normalizedRoute = route === \"/\" ? route : route.replace(/\\/+$/, \"\");\n return normalizedRoute === \"/\"\n ? pathname === normalizedRoute\n : pathname === normalizedRoute || pathname.startsWith(`${normalizedRoute}/`);\n });\n}\n\nexport {\n AppNavigation,\n AppNavigationLink,\n isNavigationPathActive,\n type AppNavigationLinkProps,\n type AppNavigationProps,\n type AppNavigationVariant,\n};\n", "type": "registry:ui" } ], "docs": "The header variant sizes its links with --app-header-height, so a sticky toolbar or table header can offset itself by the same token instead of hard-coding the height.", "type": "registry:ui" }