{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "navigation-menu",
  "type": "registry:ui",
  "title": "Navigation Menu",
  "description": "An accessible React navigation menu for Next.js and Tailwind CSS: a top bar with rich dropdown panels that glide between sizes, built on Radix Navigation Menu.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.52.0",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/navigation-menu.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { blur, duration, ease, spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport {\n  type AnimationPlaybackControls,\n  animate,\n  LayoutGroup,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport { NavigationMenu as NavigationMenuPrimitive, Slot } from \"radix-ui\";\nimport {\n  type ComponentProps,\n  createContext,\n  type Ref,\n  type RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\n/*\n * Navigation Menu, built on Radix Navigation Menu (roadmap, 2026-10-16: \"A top bar with rich dropdown panels\").\n * A nav landmark of links and disclosure buttons, each button opening a panel of links. Hover opens after a\n * short delay; click, Enter and Space toggle; ArrowDown opens and moves into the panel; Escape closes and hands\n * focus back to the trigger; Tab runs through the open panel's links.\n * - One highlight glides between the top-level items (layoutId on spring.snappy, scoped to each menu). It\n *   follows the mouse, rests on the open trigger otherwise, and fades out where it was when neither applies.\n * - The panels share one viewport, a floating surface under the list. Moving between triggers, its width and\n *   height glide to the next panel's size (a CSS transition on the size Radix measures, duration-base on\n *   ease-out), while the new panel slides 12px in from the side you moved towards through the blur and the\n *   old one slides out the other way.\n * - The viewport comes through the blur from the list (popover-in, from its top edge) and leaves quickly\n *   (popover-out). An optional caret on its top edge points at the open trigger and slides along with it.\n * - Each trigger's chevron turns over while its panel is open (spring.snappy).\n * - Inside a panel, one highlight glides between the links too (the same layoutId pill, one per panel), on\n *   hover and on keyboard focus, and fades out where it was when you leave. Opening the menu, the links rise\n *   into place through the blur, 0.05s apart; switching panels they slide in with the panel instead.\n * - Nothing animates on first paint. With reduced motion everything appears and moves at once.\n */\n\n/** How far a panel slides in from (and out towards) the side you moved, in px: Tabs' distance */\nconst SLIDE = 12;\n\ntype Highlight = {\n  /** The item the highlight sits in, or null when there's none */\n  item: string | null;\n  /** False while it fades out in the item it was last in */\n  shown: boolean;\n  /**\n   * Counts appearances from nothing. Each has its own layoutId, so the highlight fades in where it appears\n   * rather than flying over from where it last faded out.\n   */\n  appearance: number;\n  /** The first item of an appearance fades the highlight in; the items after it glide it */\n  fresh: boolean;\n};\n\ntype MenuContextValue = {\n  /** The open item, or \"\" when the menu is closed */\n  value: string;\n  setValue: (value: string) => void;\n  viewport: boolean;\n  horizontal: boolean;\n  reduce: boolean;\n  highlight: Highlight;\n  /** The mouse is over an item, or has left it */\n  hover: (item: string) => void;\n  unhover: (item: string) => void;\n  /** The highlight's fade-out has finished: remove it */\n  settle: () => void;\n  /** The trigger's corner radius as a number, so motion keeps the highlight's corners true as it changes width */\n  radius: number | undefined;\n  setRadius: (radius: number) => void;\n  root: RefObject<HTMLElement | null>;\n  /** Each trigger by its item, for the caret */\n  triggers: RefObject<Map<string, HTMLButtonElement>>;\n  /** Where the caret renders: after the viewport, so it paints over the viewport's top border */\n  slot: HTMLElement | null;\n  setSlot: (node: HTMLElement | null) => void;\n};\n\nconst MenuContext = createContext<MenuContextValue | null>(null);\n/** The item a trigger or panel belongs to */\nconst ItemContext = createContext<string | null>(null);\ntype PanelContextValue = {\n  highlight: Highlight;\n  settle: () => void;\n  reduce: boolean;\n  /** The mouse is over a link, or has left it */\n  point: (link: string) => void;\n  unpoint: (link: string) => void;\n  /** A link has keyboard focus, or has lost it */\n  focus: (link: string) => void;\n  blur: (link: string) => void;\n};\n\n/** Inside a panel, links are rows that share one gliding highlight; in the list, they look like triggers */\nconst PanelContext = createContext<PanelContextValue | null>(null);\n\n/** How far the links rise into place as the menu opens, in px (the library's swap) */\nconst RISE = 6;\n/** Links come in 0.05s apart, like items in a list (motion.md) */\nconst STAGGER = 0.05;\n\n/**\n * Hands a node to our ref and to the caller's. Stable: a new callback each render would make React detach the\n * ref (null) and attach it again after the children's layout effects, so the caret, measuring in its own\n * layout effect, found no root and stayed at the menu's left edge (2026-10-21).\n */\nfunction useMergedRef<T>(own: { current: T | null }, theirs?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      own.current = node;\n      if (typeof theirs === \"function\") theirs(node);\n      else if (theirs) (theirs as { current: T | null }).current = node;\n    },\n    [own, theirs],\n  );\n}\n\n/** A node's left offset inside an ancestor, in layout pixels (zoom and transforms don't count) */\nfunction offsetWithin(node: HTMLElement, ancestor: HTMLElement) {\n  let left = 0;\n  let current: HTMLElement | null = node;\n  while (current && current !== ancestor) {\n    left += current.offsetLeft;\n    current = current.offsetParent as HTMLElement | null;\n  }\n  return left;\n}\n\n/**\n * Where the highlight is. `target` is the hovered item, or else the open one. A new target glides the\n * highlight there; none fades it out in place; back on an item while it's still fading, it glides on from there.\n */\nfunction useHighlight(target: string | null, reduce: boolean) {\n  const [highlight, setHighlight] = useState<Highlight>({\n    item: target,\n    shown: target !== null,\n    appearance: 0,\n    fresh: false,\n  });\n  const [last, setLast] = useState(target);\n  // Follows the target in the same render, so the highlight never trails a frame behind the open trigger\n  if (target !== last) {\n    setLast(target);\n    setHighlight((current) => {\n      // With reduced motion there's no fade to wait for\n      if (target === null)\n        return reduce ? { ...current, item: null, shown: false } : { ...current, shown: false };\n      if (current.item === null)\n        return { item: target, shown: true, appearance: current.appearance + 1, fresh: true };\n      return { ...current, item: target, shown: true, fresh: false };\n    });\n  }\n  const settle = useCallback(() => {\n    setHighlight((current) => (current.shown ? current : { ...current, item: null }));\n  }, []);\n  return { highlight, settle };\n}\n\nexport type NavigationMenuProps = ComponentProps<typeof NavigationMenuPrimitive.Root> & {\n  /**\n   * Panels open in one shared viewport under the list, which glides between their sizes. Turn it off to open\n   * each panel under its own trigger instead.\n   */\n  viewport?: boolean;\n};\n\n/**\n * A site's navigation: a nav landmark (Radix names it \"Main\"; pass aria-label for another name). Holds the open\n * item, controlled or not, so the highlight, the panels and the caret know which one is open.\n */\nfunction NavigationMenu({\n  value,\n  defaultValue,\n  onValueChange,\n  viewport = true,\n  orientation = \"horizontal\",\n  className,\n  children,\n  ref,\n  ...props\n}: NavigationMenuProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const current = value ?? internal;\n  const root = useRef<HTMLElement>(null);\n  const setRef = useMergedRef(root, ref);\n  const triggers = useRef(new Map<string, HTMLButtonElement>());\n  const [slot, setSlot] = useState<HTMLElement | null>(null);\n  const [radius, setRadius] = useState<number>();\n  const [hovered, setHovered] = useState<string | null>(null);\n  const { highlight, settle } = useHighlight(hovered ?? (current || null), reduce);\n\n  const setValue = useCallback(\n    (next: string) => {\n      setInternal(next);\n      onValueChange?.(next);\n    },\n    [onValueChange],\n  );\n  const hover = useCallback((item: string) => setHovered(item), []);\n  const unhover = useCallback(\n    (item: string) => setHovered((currentItem) => (currentItem === item ? null : currentItem)),\n    [],\n  );\n\n  const menu = useMemo<MenuContextValue>(\n    () => ({\n      value: current,\n      setValue,\n      viewport,\n      horizontal: orientation === \"horizontal\",\n      reduce,\n      highlight,\n      hover,\n      unhover,\n      settle,\n      radius,\n      setRadius,\n      root,\n      triggers,\n      slot,\n      setSlot,\n    }),\n    [current, setValue, viewport, orientation, reduce, highlight, hover, unhover, settle, radius, slot],\n  );\n\n  return (\n    <MenuContext.Provider value={menu}>\n      <NavigationMenuPrimitive.Root\n        ref={setRef}\n        data-slot=\"navigation-menu\"\n        data-viewport={viewport}\n        value={current}\n        onValueChange={setValue}\n        orientation={orientation}\n        className={cn(\n          // h-max: never stretched by a flex row, since the viewport hangs from its bottom edge\n          \"group/navigation-menu relative flex h-max max-w-max flex-1 items-center justify-center\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {viewport ? <NavigationMenuViewport /> : null}\n      </NavigationMenuPrimitive.Root>\n    </MenuContext.Provider>\n  );\n}\n\n/**\n * The row of items. Its own layer, so the highlight passes behind every label as it glides. Without the\n * viewport the panels open inside that layer, so it rises to the popover level while one is there.\n */\nfunction NavigationMenuList({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof NavigationMenuPrimitive.List>) {\n  const groupId = useId();\n  return (\n    <NavigationMenuPrimitive.List\n      data-slot=\"navigation-menu-list\"\n      className={cn(\n        \"relative isolate flex flex-1 list-none items-center justify-center gap-1 has-data-[slot=navigation-menu-content]:z-(--z-popover)\",\n        className,\n      )}\n      {...props}\n    >\n      <LayoutGroup id={groupId}>{children}</LayoutGroup>\n    </NavigationMenuPrimitive.List>\n  );\n}\n\n/**\n * The highlight, inside the item that has it. Items aren't stacking contexts: it joins the list's layer (the\n * list is `isolate`), so it passes behind every label whichever way it glides. A fill, so it only fades.\n */\nfunction ItemHighlight({ item }: { item: string }) {\n  const menu = useContext(MenuContext);\n  const node = useRef<HTMLSpanElement>(null);\n  const here = menu?.highlight.item === item;\n  const radius = menu?.radius;\n  const setRadius = menu?.setRadius;\n\n  // Read once from the class, then motion keeps the corners true while the highlight changes width\n  useLayoutEffect(() => {\n    if (here && radius === undefined && node.current && setRadius)\n      setRadius(Number.parseFloat(getComputedStyle(node.current).borderTopLeftRadius) || 0);\n  }, [here, radius, setRadius]);\n\n  if (!menu || !here) return null;\n  const { highlight, reduce, settle } = menu;\n  return (\n    <motion.span\n      ref={node}\n      layoutId={`navigation-menu-highlight-${highlight.appearance}`}\n      aria-hidden\n      data-slot=\"navigation-menu-highlight\"\n      className=\"pointer-events-none absolute inset-0 -z-10 rounded-md bg-accent\"\n      style={radius ? { borderRadius: radius } : undefined}\n      initial={highlight.fresh ? { opacity: 0 } : false}\n      animate={{ opacity: highlight.shown ? 1 : 0 }}\n      transition={\n        reduce ? { duration: 0 } : { ...spring.snappy, opacity: { duration: duration.fast, ease: ease.out } }\n      }\n      onAnimationComplete={() => {\n        if (!highlight.shown) settle();\n      }}\n    />\n  );\n}\n\n/** One item of the list: a trigger with its panel, or a link */\nfunction NavigationMenuItem({\n  className,\n  value,\n  children,\n  onPointerEnter,\n  onPointerLeave,\n  ...props\n}: ComponentProps<typeof NavigationMenuPrimitive.Item>) {\n  const menu = useContext(MenuContext);\n  const auto = useId();\n  // Radix makes its own when there's none; ours is the same idea, so we know which item is open\n  const item = value || auto;\n\n  return (\n    <ItemContext.Provider value={item}>\n      <NavigationMenuPrimitive.Item\n        data-slot=\"navigation-menu-item\"\n        value={item}\n        className={cn(\"relative\", className)}\n        onPointerEnter={(event) => {\n          onPointerEnter?.(event);\n          // Mouse only: on touch the highlight just marks the open trigger. Never on a disabled trigger.\n          if (event.pointerType !== \"mouse\" || event.currentTarget.querySelector(\":scope > [data-disabled]\"))\n            return;\n          menu?.hover(item);\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          if (event.pointerType === \"mouse\") menu?.unhover(item);\n        }}\n        {...props}\n      >\n        <ItemHighlight item={item} />\n        {children}\n      </NavigationMenuPrimitive.Item>\n    </ItemContext.Provider>\n  );\n}\n\n/**\n * The look of a top-level item: a trigger, or a link in the list. Flat: the hover and open fill is the\n * gliding highlight, not a background of its own. Muted labels turn foreground on hover, open or current.\n */\nconst navigationMenuTriggerStyle = cva([\n  \"relative inline-flex h-(--size-control-md) w-max cursor-pointer items-center justify-center gap-1 rounded-md px-3 text-sm font-medium whitespace-nowrap\",\n  \"text-muted-foreground transition-colors duration-(--duration-fast) ease-out hover:text-foreground data-active:text-foreground data-[state=open]:text-foreground\",\n  \"outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n  \"disabled:pointer-events-none disabled:opacity-(--opacity-disabled)\",\n  \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md)\",\n]);\n\n/** The button that opens an item's panel. Its chevron turns over while the panel is open. */\nfunction NavigationMenuTrigger({\n  className,\n  children,\n  onKeyDown,\n  ref,\n  ...props\n}: ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {\n  const menu = useContext(MenuContext);\n  const item = useContext(ItemContext);\n  const open = menu !== null && item !== null && menu.value === item;\n  const node = useRef<HTMLButtonElement>(null);\n  const setRef = useMergedRef(node, ref);\n  const entering = useRef(false);\n  const triggers = menu?.triggers;\n\n  // Known to the caret, which slides to the open trigger\n  useLayoutEffect(() => {\n    const trigger = node.current;\n    const map = triggers?.current;\n    if (!trigger || !map || item === null) return;\n    map.set(item, trigger);\n    return () => {\n      if (map.get(item) === trigger) map.delete(item);\n    };\n  }, [triggers, item]);\n\n  // Opened with ArrowDown: once the panel is in, move into it the way Radix does for ArrowDown on an open\n  // trigger (its own handler also gives the links back their tab order)\n  useEffect(() => {\n    if (!open || !entering.current) return;\n    entering.current = false;\n    const frame = requestAnimationFrame(() =>\n      node.current?.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"ArrowDown\", bubbles: true })),\n    );\n    return () => cancelAnimationFrame(frame);\n  }, [open]);\n\n  return (\n    <NavigationMenuPrimitive.Trigger\n      ref={setRef}\n      data-slot=\"navigation-menu-trigger\"\n      className={cn(navigationMenuTriggerStyle(), className)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (event.defaultPrevented || !menu || item === null) return;\n        // On a closed trigger, Radix would move to the next one; opening is what ArrowDown means here\n        if (event.key === \"ArrowDown\" && menu.horizontal && !open) {\n          event.preventDefault();\n          entering.current = true;\n          menu.setValue(item);\n        }\n      }}\n      {...props}\n    >\n      {children}\n      <motion.span\n        aria-hidden\n        className=\"inline-flex\"\n        initial={false}\n        animate={{ rotate: open ? 180 : 0 }}\n        transition={menu?.reduce ? { duration: 0 } : spring.snappy}\n      >\n        <ChevronDownIcon className=\"size-(--size-icon-sm)\" />\n      </motion.span>\n    </NavigationMenuPrimitive.Trigger>\n  );\n}\n\ntype PanelProps = ComponentProps<\"div\"> & {\n  viewport: boolean;\n  leaving: boolean;\n  onLeft: () => void;\n  /** Set by Radix: the side the panel enters from or leaves towards (from-start, to-end...) */\n  \"data-motion\"?: string | null;\n};\n\n/**\n * The panel's own element: Radix renders the content through it (asChild), handing over its attributes. In the\n * viewport it plays the slide; without one it's a floating surface of its own, under its trigger.\n */\nfunction NavigationMenuPanel({ viewport, leaving, onLeft, className, children, ref, ...props }: PanelProps) {\n  const menu = useContext(MenuContext);\n  const reduce = menu?.reduce ?? false;\n  const node = useRef<HTMLDivElement>(null);\n  const setRef = useMergedRef(node, ref);\n  const fresh = useRef(true);\n  const side = props[\"data-motion\"];\n\n  // One highlight for the panel's links: the one under the mouse, else the one with keyboard focus\n  const groupId = useId();\n  const [pointed, setPointed] = useState<string | null>(null);\n  const [focused, setFocused] = useState<string | null>(null);\n  const { highlight, settle } = useHighlight(pointed ?? focused, reduce);\n  const panel = useMemo<PanelContextValue>(\n    () => ({\n      highlight,\n      settle,\n      reduce,\n      point: (link) => setPointed(link),\n      unpoint: (link) => setPointed((current) => (current === link ? null : current)),\n      focus: (link) => setFocused(link),\n      blur: (link) => setFocused((current) => (current === link ? null : current)),\n    }),\n    [highlight, settle, reduce],\n  );\n\n  // Before the first paint, so a panel never flashes in place before it slides\n  // biome-ignore lint/correctness/useExhaustiveDependencies: only arriving or leaving starts a slide\n  useLayoutEffect(() => {\n    const panel = node.current;\n    const first = fresh.current;\n    fresh.current = false;\n    if (!panel || !viewport || reduce) return;\n    // Radix's start and end are the left and right of the screen (it flips them for right-to-left pages)\n    const towards = side?.endsWith(\"start\") ? -1 : 1;\n    let live = true;\n    let running: AnimationPlaybackControls | undefined;\n\n    if (leaving) {\n      // Slides out the other way through the blur, quicker than the new panel comes in, so the two barely overlap\n      const exit = animate(\n        panel,\n        { opacity: 0, x: towards * SLIDE, filter: blur.hidden },\n        { duration: duration.fast, ease: ease.out },\n      );\n      exit.then(() => {\n        if (live) onLeft();\n      });\n      running = exit;\n    } else if (first && !side) {\n      // Opening the menu (not switching panels): the links rise into place through the blur, in turn\n      const links = Array.from(panel.querySelectorAll<HTMLElement>(\"[data-slot=navigation-menu-link]\"));\n      const rises = links.map((link, index) =>\n        animate(\n          link,\n          { opacity: [0, 1], y: [RISE, 0], filter: [blur.hidden, blur.shown] },\n          { duration: duration.slow, ease: ease.out, delay: index * STAGGER },\n        ),\n      );\n      const clear = () => {\n        for (const link of links)\n          for (const property of [\"opacity\", \"transform\", \"filter\"]) link.style.removeProperty(property);\n      };\n      Promise.all(rises).then(() => {\n        if (live) clear();\n      });\n      return () => {\n        live = false;\n        for (const rise of rises) rise.stop();\n        clear();\n      };\n    } else if (side === \"from-start\" || side === \"from-end\") {\n      // A panel that was sliding away and is wanted back turns round from where it is\n      const entrance = animate(\n        panel,\n        first\n          ? { opacity: [0, 1], x: [towards * SLIDE, 0], filter: [blur.hidden, blur.shown] }\n          : { opacity: 1, x: 0, filter: blur.shown },\n        { duration: duration.base, ease: ease.out },\n      );\n      // A filter left behind would make a stacking context and a containing block round the links\n      entrance.then(() => {\n        if (live) panel.style.removeProperty(\"filter\");\n      });\n      running = entrance;\n    }\n    return () => {\n      live = false;\n      running?.stop();\n    };\n  }, [leaving, side]);\n\n  return (\n    <div\n      {...props}\n      ref={setRef}\n      data-slot=\"navigation-menu-content\"\n      // On its way out: out of reach for the pointer, the keyboard and screen readers\n      inert={leaving || undefined}\n      className={cn(\n        viewport\n          ? // Centred, so a panel of another size stays put while the viewport glides round it. w-max: the panel\n            // is as wide as its content, never squeezed by the viewport it's about to resize. Its own layer, so\n            // the links' highlight passes behind every row whichever way it glides.\n            \"absolute top-0 left-1/2 isolate w-max -translate-x-1/2 p-2\"\n          : \"absolute start-0 top-full isolate z-(--z-popover) mt-2 w-max origin-top-left rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-md data-[state=open]:animate-popover-in data-[state=closed]:animate-popover-out rtl:origin-top-right\",\n        leaving && \"pointer-events-none\",\n        className,\n      )}\n    >\n      <PanelContext.Provider value={panel}>\n        <LayoutGroup id={groupId}>{children}</LayoutGroup>\n      </PanelContext.Provider>\n    </div>\n  );\n}\n\nexport type NavigationMenuContentProps = Omit<\n  ComponentProps<typeof NavigationMenuPrimitive.Content>,\n  \"forceMount\" | \"asChild\"\n>;\n\n/**\n * A trigger's panel. Give what's inside a width (`w-md`, a grid), so the viewport knows the size to glide to.\n */\nfunction NavigationMenuContent({ className, children, ref, ...props }: NavigationMenuContentProps) {\n  const menu = useContext(MenuContext);\n  const item = useContext(ItemContext);\n  const viewport = menu?.viewport ?? true;\n  const active = menu !== null && item !== null && menu.value === item;\n  // Moving to another panel in the viewport, this one stays mounted while it slides away. When the menu closes\n  // it goes with the viewport instead.\n  const switching = viewport && !(menu?.reduce ?? false) && (menu?.value ?? \"\") !== \"\";\n  const [was, setWas] = useState(active);\n  const [leaving, setLeaving] = useState(false);\n  if (was !== active) {\n    setWas(active);\n    setLeaving(!active && switching);\n  } else if (leaving && !switching) setLeaving(false);\n  const left = useCallback(() => setLeaving(false), []);\n\n  return (\n    <NavigationMenuPrimitive.Content\n      // In the viewport the shown panel and the one sliding away stay mounted: they play their own motion\n      forceMount={viewport && (active || leaving) ? true : undefined}\n      {...props}\n      asChild\n    >\n      <NavigationMenuPanel\n        ref={ref}\n        viewport={viewport}\n        leaving={leaving}\n        onLeft={left}\n        className={className}\n      >\n        {children}\n      </NavigationMenuPanel>\n    </NavigationMenuPrimitive.Content>\n  );\n}\n\n/** A panel row: a title over a muted description. The hover fill is the panel's gliding highlight. */\nconst panelLinkClasses = [\n  \"relative flex flex-col gap-1 rounded-md p-3 text-sm outline-ring transition-colors duration-(--duration-fast) ease-out\",\n  \"hover:text-accent-foreground data-active:bg-accent/50 data-active:text-accent-foreground\",\n  \"focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n  \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md) [&_svg:not([class*='text-'])]:text-muted-foreground\",\n];\n\n/**\n * A link. In the list it looks like a trigger; inside a panel it's a row. Selecting it closes the menu. Use\n * asChild with your router's link, and `active` for the current page.\n */\nfunction NavigationMenuLink({\n  className,\n  children,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: ComponentProps<typeof NavigationMenuPrimitive.Link>) {\n  const panel = useContext(PanelContext);\n  const link = useId();\n  if (!panel)\n    return (\n      <NavigationMenuPrimitive.Link\n        data-slot=\"navigation-menu-link\"\n        className={cn(navigationMenuTriggerStyle(), className)}\n        onPointerEnter={onPointerEnter}\n        onPointerLeave={onPointerLeave}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        {...props}\n      >\n        {children}\n      </NavigationMenuPrimitive.Link>\n    );\n  return (\n    <NavigationMenuPrimitive.Link\n      data-slot=\"navigation-menu-link\"\n      className={cn(panelLinkClasses, className)}\n      onPointerEnter={(event) => {\n        onPointerEnter?.(event);\n        // Mouse only: a tap goes straight to the page\n        if (event.pointerType === \"mouse\") panel.point(link);\n      }}\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event);\n        if (event.pointerType === \"mouse\") panel.unpoint(link);\n      }}\n      onFocus={(event) => {\n        onFocus?.(event);\n        panel.focus(link);\n      }}\n      onBlur={(event) => {\n        onBlur?.(event);\n        panel.blur(link);\n      }}\n      {...props}\n    >\n      <PanelHighlight link={link} />\n      {/* With asChild the child (your router's link) becomes the row, and the highlight goes inside it */}\n      <Slot.Slottable>{children}</Slot.Slottable>\n    </NavigationMenuPrimitive.Link>\n  );\n}\n\n/**\n * The panel's highlight, inside the link that has it. Links aren't stacking contexts: it joins the panel's\n * layer (the panel is `isolate`), so it passes behind every row's text. A fill, so it only fades.\n */\nfunction PanelHighlight({ link }: { link: string }) {\n  const panel = useContext(PanelContext);\n  if (!panel || panel.highlight.item !== link) return null;\n  const { highlight, reduce, settle } = panel;\n  return (\n    <motion.span\n      layoutId={`navigation-menu-panel-highlight-${highlight.appearance}`}\n      aria-hidden\n      data-slot=\"navigation-menu-panel-highlight\"\n      className=\"pointer-events-none absolute inset-0 -z-10 rounded-md bg-accent\"\n      initial={highlight.fresh ? { opacity: 0 } : false}\n      animate={{ opacity: highlight.shown ? 1 : 0 }}\n      transition={\n        reduce ? { duration: 0 } : { ...spring.snappy, opacity: { duration: duration.fast, ease: ease.out } }\n      }\n      onAnimationComplete={() => {\n        if (!highlight.shown) settle();\n      }}\n    />\n  );\n}\n\n/**\n * The floating surface the panels open in, under the list. NavigationMenu renders it unless `viewport` is\n * false. Its width and height are Radix's measure of the open panel; changing panels, they glide there.\n */\nfunction NavigationMenuViewport({\n  className,\n  ...props\n}: ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {\n  const menu = useContext(MenuContext);\n  return (\n    // Spans the menu and centres the viewport under it. Only the viewport takes the pointer.\n    <div className=\"pointer-events-none absolute inset-x-0 top-full z-(--z-popover) flex justify-center\">\n      <NavigationMenuPrimitive.Viewport\n        data-slot=\"navigation-menu-viewport\"\n        className={cn(\n          // box-content: the measured size is the panel's, and the border sits outside it\n          \"pointer-events-auto relative mt-2 box-content h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) shrink-0 origin-top overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md\",\n          // The size glides between panels (no transition on the first measure: there's nothing to glide from).\n          // Height and width move here, a documented exception: decisions.md. Instant with reduced motion.\n          \"[transition-property:width,height] duration-(--duration-base) ease-out\",\n          \"data-[state=open]:animate-popover-in data-[state=closed]:animate-popover-out\",\n          className,\n        )}\n        {...props}\n      />\n      {/* After the viewport, so the caret paints over its top border */}\n      <span ref={menu?.setSlot} className=\"pointer-events-none absolute inset-x-0 top-0 h-2\" />\n    </div>\n  );\n}\n\n/** The caret itself, on the viewport's top edge */\nfunction Caret({ menu, className }: { menu: MenuContextValue; className?: string }) {\n  const { value, reduce, root, triggers, slot } = menu;\n  const open = value !== \"\";\n  const x = useMotionValue(0);\n  // It has a place in this opening: from here on it slides; the first place of an opening it jumps to\n  const placed = useRef(false);\n\n  useLayoutEffect(() => {\n    if (!open) {\n      placed.current = false;\n      return;\n    }\n    let last: number | undefined;\n    const place = (glide: boolean) => {\n      const trigger = triggers.current.get(value);\n      const box = root.current;\n      if (!trigger || !box || !slot) return;\n      // Offsets, not on-screen boxes, so a zoomed or scaled page doesn't double the trip\n      const centre = offsetWithin(trigger, box) + trigger.offsetWidth / 2 - offsetWithin(slot, box);\n      if (centre === last) return;\n      last = centre;\n      // jump() resets the speed, so a later slide never starts with the jump counted as velocity\n      if (glide && placed.current && !reduce) animate(x, centre, spring.snappy);\n      else x.jump(centre);\n      placed.current = true;\n    };\n    place(true);\n    // Fonts loading or the page resizing move the triggers: follow without sliding\n    const observer = new ResizeObserver(() => place(false));\n    if (root.current) observer.observe(root.current);\n    return () => observer.disconnect();\n  }, [open, value, reduce, root, triggers, slot, x]);\n\n  useEffect(() => () => x.stop(), [x]);\n\n  return (\n    <motion.span\n      aria-hidden\n      data-slot=\"navigation-menu-indicator\"\n      data-state={open ? \"visible\" : \"hidden\"}\n      // Its base covers the viewport's top border, so the caret and the surface read as one shape\n      className={cn(\"absolute bottom-0 left-0 flex h-1.5 w-3 -translate-x-1/2 translate-y-px\", className)}\n      style={{ x }}\n      initial={false}\n      // With the viewport's own timing: in on popover-in's, out on popover-out's. A fill, so it only fades.\n      animate={{ opacity: open ? 1 : 0 }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : open\n            ? { duration: duration.fast, ease: ease.out }\n            : { duration: duration.instant, ease: ease.in }\n      }\n    >\n      <svg viewBox=\"0 0 12 6\" fill=\"none\" aria-hidden className=\"size-full overflow-visible\">\n        <path d=\"M0 6 6 0l6 6\" className=\"fill-popover stroke-border\" />\n      </svg>\n    </motion.span>\n  );\n}\n\n/**\n * A caret on the viewport's top edge, pointing at the open trigger; it slides along when another opens. Place\n * it in NavigationMenuList, as on shadcn. It needs the viewport, so it renders nothing with `viewport={false}`.\n */\nfunction NavigationMenuIndicator({ className }: { className?: string }) {\n  const menu = useContext(MenuContext);\n  if (!menu?.slot) return null;\n  return createPortal(<Caret menu={menu} className={className} />, menu.slot);\n}\n\nexport {\n  NavigationMenu,\n  NavigationMenuContent,\n  NavigationMenuIndicator,\n  NavigationMenuItem,\n  NavigationMenuLink,\n  NavigationMenuList,\n  NavigationMenuTrigger,\n  NavigationMenuViewport,\n  navigationMenuTriggerStyle,\n};\n"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/navigation-menu"
}
