'use client'
import * as React from 'react'
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '../../lib/utils'
export interface GroupedNavLink {
href: string
label: string
icon?: React.ReactNode
/** Optional trailing slot (count pill, status dot…). */
badge?: React.ReactNode
}
export interface GroupedNavGroup {
label: string
icon?: React.ReactNode
items: GroupedNavLink[]
/** Start expanded (default true). Ignored while a child is active — then forced open. */
defaultOpen?: boolean
/** Show a chevron toggle (default true). false = a static section heading. */
collapsible?: boolean
}
/** A top-level link, or a named group of links. Discriminated by `items`. */
export type GroupedNavEntry = GroupedNavLink | GroupedNavGroup
export interface GroupedNavRenderLinkArgs {
link: GroupedNavLink
active: boolean
className: string
/** The pre-composed inner content (icon + label + badge). */
content: React.ReactNode
}
export interface GroupedNavProps {
items: GroupedNavEntry[]
/** The current path; drives active state. */
activeHref?: string
/** Override active matching (default: exact, or `activeHref` under `href/`). */
isActive?: (href: string, activeHref?: string) => boolean
/**
* Render a link — pass `next/link` here in a Next app. Default renders a plain
* `` so the primitive stays framework-agnostic.
*/
renderLink?: (args: GroupedNavRenderLinkArgs) => React.ReactNode
className?: string
}
function isGroup(entry: GroupedNavEntry): entry is GroupedNavGroup {
return Array.isArray((entry as GroupedNavGroup).items)
}
function defaultIsActive(href: string, activeHref?: string): boolean {
if (!activeHref) return false
if (href === '/') return activeHref === '/'
return activeHref === href || activeHref.startsWith(href + '/')
}
const defaultRenderLink = ({ link, active, className, content }: GroupedNavRenderLinkArgs) => (
{content}
)
function linkClasses(active: boolean, indented: boolean) {
return cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
indented && 'pl-9',
active
? 'bg-primary text-primary-foreground'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900',
)
}
/**
* A grouped sidebar navigation list: a mix of top-level links and collapsible
* groups (e.g. Dashboard + a "Content" group of the three content types). Purely
* the `