"use client"
import { createContext, use } from "react"
import type { CSSProperties } from "react"
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { ChevronDown } from "lucide-react"
import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import {
color,
duration,
ease,
leading,
radius,
shadow,
space,
text,
} from "@/theme/tokens.stylex"
const styles = stylex.create({
root: {
position: "relative",
display: "flex",
maxWidth: "max-content",
alignItems: "center",
justifyContent: "center",
},
list: {
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: space.s1,
},
listVertical: { flexDirection: "column", alignItems: "stretch" },
item: { position: "relative" },
trigger: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: space.s1,
height: space.s8,
width: "max-content",
borderRadius: radius.lg,
paddingInline: space.s2_5,
paddingBlock: space.s1_5,
backgroundColor: {
default: `var(--carbon-nav-current-bg, ${color.transparent})`,
":hover": color.itemHighlightBg,
},
fontSize: text.sm,
lineHeight: leading.sm,
fontWeight: 500,
transitionProperty: "color, background-color, box-shadow",
transitionDuration: duration.color,
transitionTimingFunction: ease.standard,
pointerEvents: { default: null, ":disabled": "none" },
opacity: { default: 1, ":disabled": 0.5 },
"--carbon-icon-size": space.s4,
},
surface: {
boxShadow: {
default: `var(--carbon-nav-current-ring, inset 0 0 0 0 ${color.transparent})`,
":focus-visible": `var(--carbon-nav-current-ring, inset 0 0 0 0 ${color.transparent}), ${shadow.ring}`,
},
},
current: {
"--carbon-nav-current-bg": color.itemHighlightBg,
"--carbon-nav-current-ring": `inset 0 0 0 1px ${color.itemHighlightRing}`,
},
chevron: {
display: "inline-flex",
position: "relative",
top: 1,
"--carbon-icon-size": space.s3,
transitionProperty: "transform",
transitionDuration: { default: duration.move, "@media (prefers-reduced-motion: reduce)": "0s" },
transitionTimingFunction: ease.standard,
},
chevronOpen: { transform: "rotate(180deg)" },
link: {
display: "flex",
alignItems: "center",
gap: space.s2,
borderRadius: radius.lg,
paddingInline: space.s2,
paddingBlock: space.s2,
backgroundColor: {
default: `var(--carbon-nav-current-bg, ${color.transparent})`,
":hover": color.itemHighlightBg,
},
fontSize: text.sm,
lineHeight: leading.sm,
transitionProperty: "color, background-color, box-shadow",
transitionDuration: duration.color,
transitionTimingFunction: ease.standard,
"--carbon-icon-size": space.s4,
},
content: {
width: "auto",
height: "100%",
padding: space.s1,
transitionProperty: "opacity, transform",
transitionDuration: {
default: `150ms, ${duration.panel}`,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: `${ease.standard}, ${ease.decelerate}`,
},
hidden: { opacity: 0 },
positioner: {
width: "var(--positioner-width)",
height: "var(--positioner-height)",
maxWidth: "var(--available-width)",
transitionProperty: "top, left, right, bottom",
transitionDuration: {
default: duration.panel,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.decelerate,
"::before": { content: "''", position: "absolute" },
},
positionerInstant: { transitionDuration: "0s" },
popup: {
position: "relative",
width: "var(--popup-width)",
height: "var(--popup-height)",
"--carbon-popup-radius": radius.lg,
borderRadius: "var(--carbon-popup-radius)",
transformOrigin: "var(--transform-origin)",
transitionProperty: "opacity, transform, width, height",
transitionDuration: {
default: duration.panel,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.decelerate,
},
popupHidden: { opacity: 0, transform: "scale(0.95)" },
popupEnding: {
transitionDuration: { default: "150ms", "@media (prefers-reduced-motion: reduce)": "0s" },
transitionTimingFunction: ease.standard,
},
viewport: { position: "relative", width: "100%", height: "100%", overflow: "hidden" },
})
const bridge = stylex.create({
bottom: {
"::before": { top: `calc(-1 * ${space.s2_5})`, right: 0, left: 0, height: space.s2_5 },
},
top: {
"::before": { bottom: `calc(-1 * ${space.s2_5})`, right: 0, left: 0, height: space.s2_5 },
},
left: {
"::before": { top: 0, bottom: 0, right: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
},
right: {
"::before": { top: 0, bottom: 0, left: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
},
"inline-start": {
"::before": { top: 0, bottom: 0, right: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
},
"inline-end": {
"::before": { top: 0, bottom: 0, left: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
},
})
const slide = stylex.create({
left: { transform: "translateX(-50%)" },
right: { transform: "translateX(50%)" },
up: { transform: "translateY(-50%)" },
down: { transform: "translateY(50%)" },
})
const opposite = { left: "right", right: "left", up: "down", down: "up" } as const
type Orientation = "horizontal" | "vertical"
type Overrides = { xstyle?: StyleXStyles; className?: string }
type StateOverrides = Overrides & { style?: CSSProperties }
const NavigationMenuOrientationContext = createContext<Orientation>("horizontal")
const NavigationMenuContentContext = createContext(false)
type NavigationMenuPositionerProps = Omit<
NavigationMenuPrimitive.Positioner.Props,
"className" | "style"
> & {
style?: CSSProperties
popupProps?: Omit<NavigationMenuPrimitive.Popup.Props, "className" | "style"> & StateOverrides
}
function NavigationMenuPositioner({
side = "bottom",
sideOffset = 8,
align = "start",
alignOffset = 0,
popupProps,
style,
...props
}: NavigationMenuPositionerProps) {
const { xstyle, className, style: popupStyle, ...popup } = popupProps ?? {}
return (
<NavigationMenuPrimitive.Portal>
<NavigationMenuPrimitive.Positioner
data-slot="navigation-menu-positioner"
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
{...props}
{...stateStyles(
(state) => [
states.positioner,
styles.positioner,
bridge[state.side],
state.instant && styles.positionerInstant,
],
{ style },
)}
>
<NavigationMenuPrimitive.Popup
data-slot="navigation-menu-popup"
{...popup}
{...stateStyles(
(state) => [
states.popupSurface,
styles.popup,
(state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
styles.popupHidden,
state.transitionStatus === "ending" && styles.popupEnding,
xstyle,
],
{ className, style: popupStyle },
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
{...stylex.props(styles.viewport)}
/>
</NavigationMenuPrimitive.Popup>
</NavigationMenuPrimitive.Positioner>
</NavigationMenuPrimitive.Portal>
)
}
type NavigationMenuProps<Value> = Omit<NavigationMenuPrimitive.Root.Props<Value>, "className"> &
Pick<
NavigationMenuPositionerProps,
"side" | "sideOffset" | "align" | "alignOffset" | "popupProps"
> &
Overrides
function NavigationMenu<Value = any>({
orientation = "horizontal",
side = orientation === "vertical" ? "right" : "bottom",
sideOffset,
align,
alignOffset,
popupProps,
xstyle,
className,
children,
...props
}: NavigationMenuProps<Value>) {
const sx = stylex.props(styles.root, xstyle)
return (
<NavigationMenuOrientationContext value={orientation}>
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-orientation={orientation}
orientation={orientation}
{...sx}
{...props}
className={clsx(sx.className, className)}
>
{children}
<NavigationMenuPositioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
popupProps={popupProps}
/>
</NavigationMenuPrimitive.Root>
</NavigationMenuOrientationContext>
)
}
type NavigationMenuListProps = Omit<NavigationMenuPrimitive.List.Props, "className"> & Overrides
function NavigationMenuList({ xstyle, className, ...props }: NavigationMenuListProps) {
const orientation = use(NavigationMenuOrientationContext)
const sx = stylex.props(styles.list, orientation === "vertical" && styles.listVertical, xstyle)
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
data-orientation={orientation}
{...sx}
{...props}
className={clsx(sx.className, className)}
/>
)
}
type NavigationMenuItemProps = Omit<NavigationMenuPrimitive.Item.Props, "className"> & Overrides
function NavigationMenuItem({ xstyle, className, ...props }: NavigationMenuItemProps) {
const sx = stylex.props(styles.item, xstyle)
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
{...sx}
{...props}
className={clsx(sx.className, className)}
/>
)
}
type NavigationMenuTriggerProps = Omit<
NavigationMenuPrimitive.Trigger.Props,
"className" | "style"
> &
StateOverrides
function NavigationMenuTrigger({
xstyle,
style,
className,
children,
...props
}: NavigationMenuTriggerProps) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
{...props}
{...stateStyles(
(state) => [
styles.trigger,
states.focusRing,
styles.surface,
state.open && styles.current,
xstyle,
],
{ className, style, hook: "carbon-icons" },
)}
>
{children}
<NavigationMenuPrimitive.Icon
data-slot="navigation-menu-trigger-icon"
{...stateStyles((state) => [styles.chevron, state.open && styles.chevronOpen])}
>
<ChevronDown aria-hidden="true" />
</NavigationMenuPrimitive.Icon>
</NavigationMenuPrimitive.Trigger>
)
}
type NavigationMenuContentProps = Omit<
NavigationMenuPrimitive.Content.Props,
"className" | "style"
> &
StateOverrides
function NavigationMenuContent({ xstyle, style, className, ...props }: NavigationMenuContentProps) {
return (
<NavigationMenuContentContext value={true}>
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
{...props}
{...stateStyles(
(state) => {
const direction = state.activationDirection
const entering = state.transitionStatus === "starting"
const leaving = state.transitionStatus === "ending"
return [
styles.content,
(entering || leaving) && styles.hidden,
direction && entering && slide[direction],
direction && leaving && slide[opposite[direction]],
xstyle,
]
},
{ className, style },
)}
/>
</NavigationMenuContentContext>
)
}
type NavigationMenuLinkProps = Omit<NavigationMenuPrimitive.Link.Props, "className" | "style"> &
StateOverrides
function NavigationMenuLink({ xstyle, style, className, ...props }: NavigationMenuLinkProps) {
const inContent = use(NavigationMenuContentContext)
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
{...props}
{...stateStyles(
(state) => [
styles.link,
states.focusRing,
styles.surface,
inContent && states.popupItem,
state.active && styles.current,
xstyle,
],
{ className, style, hook: "carbon-icons" },
)}
/>
)
}
const navigationMenuStyles = {
trigger: [styles.trigger, states.focusRing, styles.surface],
link: [styles.link, states.focusRing, styles.surface],
}
export {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuPositioner,
NavigationMenuTrigger,
navigationMenuStyles,
}