{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "navigation-menu",
  "title": "Navigation Menu",
  "author": "@fma-ui/ui",
  "description": "Site-header navigation: a `<nav>` list of top-level links and disclosure dropdowns. A `NavigationMenuItem` holds either a `NavigationMenuLink` or a `NavigationMenuTrigger` (a button with `aria-expanded`) plus its `NavigationMenuContent` (a popover). Escape or a click outside closes the dropdown and returns focus to the trigger; following a link closes it too. Style plain links like triggers with `navigationMenuTriggerStyle()`, and mark the current page with `aria-current=\"page\"`. Dropdowns open on press; with `openOnHover` they also open when the pointer rests on a trigger (`delay`, `closeDelay`), as a non-modal \"peek\" that leaves focus alone, switches at once to another trigger and closes when the pointer leaves or on Escape. Pressing a peeked trigger keeps it open as a regular dropdown. Add `NavigationMenuIndicator` after the list for an arrow that slides under the open trigger. For app command menus use `Menubar` or `DropdownMenu`.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "cn@^0.4.0",
    "lucide-react@^1.52.0",
    "react-aria-components@^1.21.1"
  ],
  "files": [
    {
      "path": "src/core/navigation-menu/navigation-menu.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  Button,\n  type ButtonProps,\n  composeRenderProps,\n  Dialog,\n  Link,\n  type LinkProps,\n  Popover,\n  type PopoverProps,\n} from \"react-aria-components\";\n\ntype MenuContextValue = {\n  openId: string | null;\n  /** The open item was opened by hover: a non-modal \"peek\" that keeps focus where it was. */\n  isPeek: boolean;\n  open: (id: string | null, options?: { peek?: boolean }) => void;\n  openOnHover: boolean;\n  delay: number;\n  closeDelay: number;\n  timer: React.RefObject<ReturnType<typeof setTimeout> | undefined>;\n  /** Latest open state, for handlers react-aria may hold from an earlier render. */\n  current: React.RefObject<{ openId: string | null; isPeek: boolean }>;\n  navRef: React.RefObject<HTMLElement | null>;\n};\n\nconst MenuContext = React.createContext<MenuContextValue | null>(null);\n\ntype ItemContextValue = {\n  isOpen: boolean;\n  isPeek: boolean;\n  setOpen: (isOpen: boolean) => void;\n  /** Pointer entered/left the trigger (hover mode only). */\n  hover: (isHovered: boolean) => void;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n};\n\n// Set by an item that has a dropdown; null for plain link items.\nconst ItemContext = React.createContext<ItemContextValue | null>(null);\n\nfunction useItem(part: string) {\n  const context = React.useContext(ItemContext);\n  if (!context) throw new Error(`${part} must be used within a NavigationMenuItem`);\n  return context;\n}\n\ntype NavigationMenuProps = React.ComponentProps<\"nav\"> & {\n  /**\n   * Also open dropdowns when the pointer rests on a trigger. A hover-opened\n   * dropdown is non-modal and leaves focus alone; pressing the trigger then\n   * keeps it open as a regular (modal) dropdown. Pressing still works as\n   * without this prop, so hover is never the only way in.\n   */\n  openOnHover?: boolean;\n  /** Milliseconds the pointer rests on a trigger before it opens. @default 150 */\n  delay?: number;\n  /** Milliseconds before a hover-opened dropdown closes once the pointer leaves. @default 300 */\n  closeDelay?: number;\n};\n\nfunction NavigationMenu({\n  className,\n  openOnHover = false,\n  delay = 150,\n  closeDelay = 300,\n  ...props\n}: NavigationMenuProps) {\n  const [state, setState] = React.useState<{ openId: string | null; isPeek: boolean }>({\n    openId: null,\n    isPeek: false,\n  });\n  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const current = React.useRef(state);\n  current.current = state;\n\n  React.useEffect(() => () => clearTimeout(timer.current), []);\n\n  // A peek never holds focus, so react-aria's Escape handling never sees it;\n  // and it closes once the pointer is neither on its trigger nor its content.\n  const navRef = React.useRef<HTMLElement>(null);\n  React.useEffect(() => {\n    if (!state.isPeek) return;\n    let closing = false;\n    const close = () => setState({ openId: null, isPeek: false });\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") close();\n    };\n    const onPointerMove = (event: PointerEvent) => {\n      if (event.pointerType === \"touch\" || !(event.target instanceof Element)) return;\n      const inside =\n        event.target.closest(\"[data-slot=navigation-menu-content][data-peek]\") ||\n        (event.target.closest(\"[data-slot=navigation-menu-trigger][aria-expanded=true]\") &&\n          navRef.current?.contains(event.target));\n      if (inside) {\n        if (closing) clearTimeout(timer.current);\n        closing = false;\n      } else if (!closing) {\n        closing = true;\n        clearTimeout(timer.current);\n        timer.current = setTimeout(close, closeDelay);\n      }\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    document.addEventListener(\"pointermove\", onPointerMove);\n    return () => {\n      document.removeEventListener(\"keydown\", onKeyDown);\n      document.removeEventListener(\"pointermove\", onPointerMove);\n    };\n  }, [state.isPeek, closeDelay]);\n\n  const context = React.useMemo<MenuContextValue>(\n    () => ({\n      ...state,\n      open: (openId, options) => {\n        clearTimeout(timer.current);\n        setState({ openId, isPeek: openId !== null && !!options?.peek });\n      },\n      openOnHover,\n      delay,\n      closeDelay,\n      timer,\n      current,\n      navRef,\n    }),\n    [state, openOnHover, delay, closeDelay],\n  );\n\n  return (\n    <MenuContext.Provider value={context}>\n      <nav\n        ref={navRef}\n        data-slot=\"navigation-menu\"\n        className={cn(\n          \"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center\",\n          className,\n        )}\n        {...props}\n      />\n    </MenuContext.Provider>\n  );\n}\n\nfunction NavigationMenuList({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"navigation-menu-list\"\n      className={cn(\"flex flex-1 list-none items-center justify-center gap-0\", className)}\n      {...props}\n    />\n  );\n}\n\n/** A top-level entry: a plain `NavigationMenuLink`, or a trigger with its content. */\nfunction NavigationMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  const id = React.useId();\n  const menu = React.useContext(MenuContext);\n  // Standalone items (outside a NavigationMenu) keep their own state.\n  const [ownOpen, setOwnOpen] = React.useState(false);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const isOpen = menu ? menu.openId === id : ownOpen;\n  const isPeek = isOpen && !!menu?.isPeek;\n\n  const context = React.useMemo<ItemContextValue>(\n    () => ({\n      isOpen,\n      isPeek,\n      triggerRef,\n      setOpen: (open) => {\n        if (!menu) return setOwnOpen(open);\n        if (open) menu.open(id);\n        else if (menu.openId === id) menu.open(null);\n      },\n      hover: (isHovered) => {\n        if (!menu?.openOnHover) return;\n        const { openId, isPeek } = menu.current.current;\n        if (openId === id) return;\n        clearTimeout(menu.timer.current);\n        if (!isHovered) return;\n        // Moving from one open dropdown to another trigger switches at once.\n        if (openId !== null && isPeek) menu.open(id, { peek: true });\n        else if (openId === null)\n          menu.timer.current = setTimeout(() => menu.open(id, { peek: true }), menu.delay);\n      },\n    }),\n    [id, menu, isOpen, isPeek],\n  );\n\n  return (\n    <ItemContext.Provider value={context}>\n      <li data-slot=\"navigation-menu-item\" className={cn(\"relative\", className)} {...props} />\n    </ItemContext.Provider>\n  );\n}\n\nconst navigationMenuTriggerStyle = cva(\n  \"group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-focus-visible:bg-muted data-focus-visible:ring-3 data-focus-visible:ring-ring/50 data-hovered:bg-muted aria-expanded:bg-muted/50 aria-expanded:data-hovered:bg-muted\",\n);\n\nfunction NavigationMenuTrigger({ className, children, ...props }: ButtonProps) {\n  const { isOpen, isPeek, setOpen, hover, triggerRef } = useItem(\"NavigationMenuTrigger\");\n  return (\n    <Button\n      ref={triggerRef}\n      data-slot=\"navigation-menu-trigger\"\n      aria-expanded={isOpen}\n      // Pressing a hover-opened dropdown keeps it open (now modal) instead of closing it.\n      onPress={() => setOpen(isPeek || !isOpen)}\n      onHoverChange={hover}\n      className={composeRenderProps(className, (className) =>\n        cn(navigationMenuTriggerStyle(), className),\n      )}\n      {...props}\n    >\n      {composeRenderProps(children, (children) => (\n        <>\n          {children}\n          <ChevronDownIcon\n            aria-hidden=\"true\"\n            className=\"relative top-px ml-1 size-3 transition duration-300 group-aria-expanded/navigation-menu-trigger:rotate-180\"\n          />\n        </>\n      ))}\n    </Button>\n  );\n}\n\ntype NavigationMenuContentProps = Omit<\n  PopoverProps,\n  \"triggerRef\" | \"isOpen\" | \"onOpenChange\" | \"children\"\n> & {\n  children?: React.ReactNode;\n  /** Names the dropdown for assistive tech; defaults to the trigger's text. */\n  \"aria-label\"?: string;\n};\n\n/**\n * The dropdown of an item. Escape or a click outside closes it and returns\n * focus to the trigger; following a link inside closes it too. Opened by\n * hover (`openOnHover`), it is non-modal and closes when the pointer leaves.\n */\nfunction NavigationMenuContent({\n  className,\n  children,\n  \"aria-label\": ariaLabel,\n  placement = \"bottom start\",\n  offset = 8,\n  ...props\n}: NavigationMenuContentProps) {\n  const { isOpen, isPeek, setOpen, triggerRef } = useItem(\"NavigationMenuContent\");\n  const label = ariaLabel ?? triggerRef.current?.textContent ?? undefined;\n  return (\n    <Popover\n      // Remount when a peek turns modal: switching modality in place makes\n      // react-aria dismiss the popover.\n      key={isPeek ? \"peek\" : \"modal\"}\n      data-slot=\"navigation-menu-content\"\n      data-peek={isPeek || undefined}\n      triggerRef={triggerRef}\n      isOpen={isOpen}\n      onOpenChange={setOpen}\n      isNonModal={isPeek}\n      placement={placement}\n      offset={offset}\n      className={composeRenderProps(className, (className) =>\n        cn(\n          \"z-50 overflow-auto rounded-lg bg-popover p-1 text-popover-foreground shadow ring-1 ring-foreground/10 outline-none data-entering:animate-in data-entering:fade-in-0 data-entering:zoom-in-95 data-exiting:animate-out data-exiting:fade-out-0 data-exiting:zoom-out-95\",\n          className,\n        ),\n      )}\n      {...props}\n    >\n      {isPeek ? (\n        // A peek must not take focus, which a Dialog does on mount.\n        <div role=\"group\" aria-label={label}>\n          {children}\n        </div>\n      ) : (\n        <Dialog aria-label={label} className=\"outline-none\">\n          {children}\n        </Dialog>\n      )}\n    </Popover>\n  );\n}\n\n/**\n * Optional arrow under the open item's trigger that slides to the next one\n * when another dropdown opens. Place it inside `NavigationMenu`, after the list.\n */\nfunction NavigationMenuIndicator({ className, style, ...props }: React.ComponentProps<\"div\">) {\n  const menu = React.useContext(MenuContext);\n  const [box, setBox] = React.useState<{ left: number; width: number } | null>(null);\n  const openId = menu?.openId ?? null;\n  const navRef = menu?.navRef;\n\n  React.useLayoutEffect(() => {\n    if (openId === null) return;\n    const nav = navRef?.current;\n    const trigger = nav?.querySelector<HTMLElement>(\n      \"[data-slot=navigation-menu-trigger][aria-expanded=true]\",\n    );\n    if (!nav || !trigger) return;\n    const navBox = nav.getBoundingClientRect();\n    const triggerBox = trigger.getBoundingClientRect();\n    setBox({ left: triggerBox.left - navBox.left, width: triggerBox.width });\n  }, [openId, navRef]);\n\n  // Keeps its last position while hidden, so it fades out in place.\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-slot=\"navigation-menu-indicator\"\n      data-state={openId !== null && box ? \"visible\" : \"hidden\"}\n      className={cn(\n        \"pointer-events-none absolute top-full left-0 z-60 flex h-2 items-end justify-center overflow-hidden transition-[translate,width,opacity] duration-200 data-[state=hidden]:opacity-0\",\n        className,\n      )}\n      style={{ translate: `${box?.left ?? 0}px 0`, width: box?.width ?? 0, ...style }}\n      {...props}\n    >\n      <div className=\"relative top-[60%] size-2 rotate-45 rounded-tl-sm bg-popover ring-1 ring-foreground/10\" />\n    </div>\n  );\n}\n\nfunction NavigationMenuLink({ className, onPress, ...props }: LinkProps) {\n  const item = React.useContext(ItemContext);\n  return (\n    <Link\n      data-slot=\"navigation-menu-link\"\n      onPress={(event) => {\n        item?.setOpen(false);\n        onPress?.(event);\n      }}\n      className={composeRenderProps(className, (className) =>\n        cn(\n          \"flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none in-data-[slot=navigation-menu-content]:rounded-md data-current:bg-muted/50 data-focus-visible:bg-muted data-focus-visible:ring-3 data-focus-visible:ring-ring/50 data-hovered:bg-muted [&_svg:not([class*='size-'])]:size-4\",\n          className,\n        ),\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type { NavigationMenuContentProps, NavigationMenuProps };\nexport {\n  NavigationMenu,\n  NavigationMenuContent,\n  NavigationMenuIndicator,\n  NavigationMenuItem,\n  NavigationMenuLink,\n  NavigationMenuList,\n  NavigationMenuTrigger,\n  navigationMenuTriggerStyle,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ui/navigation-menu.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "origin": "API of shadcn/ui's NavigationMenu (via EMITTE), rebuilt on react-aria Button, Popover, Dialog and Link",
    "tags": [
      "navigation",
      "menu",
      "header",
      "react-aria"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}