{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "navigation-menu",
  "title": "Navigation Menu",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/navigation-menu/navigation-menu.component.ts",
      "content": "import {\n  AfterViewChecked,\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewChild,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiRenderTemplateDirective, afterExitAnimation, pushDismissableLayer, uniqueId } from '@/ui/popper/popper'\nimport { navigationMenuTriggerStyle } from './navigation-menu.variants'\nimport { navigationMenuContentVariants } from './navigation-menu-content.variants'\n\nexport type NavigationMenuOrientation = 'horizontal' | 'vertical'\nexport type NavigationMenuMotion = 'from-start' | 'from-end' | 'to-start' | 'to-end'\n\nconst TABBABLE =\n  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])'\nconst tabbables = (el: HTMLElement | null | undefined) =>\n  el ? [...el.querySelectorAll<HTMLElement>(TABBABLE)].filter((n) => !n.hasAttribute('disabled')) : []\nconst isMouse = (e: PointerEvent) => !e.pointerType || e.pointerType === 'mouse'\n\n/** Internal: reports a template element to its owner on create / destroy (a ref callback). */\n@Directive({ selector: '[uiNavigationMenuRef]', standalone: true })\nexport class UiNavigationMenuRefDirective implements OnInit, OnDestroy {\n  @Input({ alias: 'uiNavigationMenuRef', required: true }) ref!: (el: HTMLElement | null) => void\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n\n  ngOnInit(): void {\n    this.ref(this.el)\n  }\n\n  ngOnDestroy(): void {\n    this.ref(null)\n  }\n}\n\n/** Radix NavigationMenuContent class string (group-data viewport=false popover + link focus reset). */\nconst CONTENT_POPOVER_CLASSES =\n  'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none'\n\n/**\n * Viewport. React's NavigationMenuViewport renders a positioning wrapper\n * (`absolute top-full left-0 isolate z-50 flex justify-center`) around the Radix\n * viewport: here the host is that wrapper and the viewport div renders inside it\n * while a menu is open (or animating closed). It hosts the active content, sizes\n * itself to it through --radix-navigation-menu-viewport-width / -height, and keeps\n * the menu open while the pointer is over it.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu-viewport, [ui-navigation-menu-viewport]',\n  standalone: true,\n  imports: [UiRenderTemplateDirective],\n  host: { class: 'absolute top-full left-0 isolate z-50 flex justify-center' },\n  template: `@if (present) {\n    <div\n      #viewport\n      data-uipkge=\"\"\n      data-slot=\"navigation-menu-viewport\"\n      [attr.data-state]=\"root.isOpen ? 'open' : 'closed'\"\n      [attr.data-orientation]=\"root.orientation\"\n      [class]=\"viewportClass\"\n      [style]=\"viewportStyle\"\n      (pointerenter)=\"root.onContentEnter()\"\n      (pointerleave)=\"onPointerLeave($event)\"\n    >\n      @for (content of root.contents(); track content) {\n        @if (content.present) {\n          <ng-container [uiRenderTemplate]=\"content.template\" />\n        }\n      }\n    </div>\n  }`,\n})\nexport class UiNavigationMenuViewportComponent implements AfterViewChecked, OnDestroy {\n  readonly root = inject(UiNavigationMenuComponent)\n  @Input('class') className?: string\n  @ViewChild('viewport') viewportRef?: ElementRef<HTMLElement>\n\n  private readonly exiting = signal(false)\n  private readonly size = signal<{ width: number; height: number } | null>(null)\n  private wasOpen = false\n  private observed: HTMLElement | null = null\n  private ro: ResizeObserver | null = null\n\n  constructor() {\n    this.root.viewportComponent = this\n  }\n\n  get present(): boolean {\n    return this.root.isOpen || this.exiting()\n  }\n\n  get viewportStyle(): Record<string, string> {\n    const s = this.size()\n    const style: Record<string, string> = {}\n    if (s) {\n      for (const lib of ['radix', 'reka']) {\n        style[`--${lib}-navigation-menu-viewport-width`] = `${s.width}px`\n        style[`--${lib}-navigation-menu-viewport-height`] = `${s.height}px`\n      }\n    }\n    if (!this.root.isOpen) style['pointer-events'] = 'none'\n    return style\n  }\n\n  beginExit(): void {\n    this.exiting.set(true)\n  }\n\n  onPointerLeave(event: PointerEvent): void {\n    if (isMouse(event)) this.root.onContentLeave()\n  }\n\n  ngAfterViewChecked(): void {\n    const open = this.root.isOpen\n    if (open !== this.wasOpen) {\n      this.wasOpen = open\n      if (!open) {\n        const el = this.viewportRef?.nativeElement\n        void afterExitAnimation(el).then(() => {\n          if (!this.root.isOpen) this.exiting.set(false)\n        })\n      } else {\n        this.exiting.set(false)\n      }\n    }\n    // Radix sizes the viewport to the active content (offsetWidth / offsetHeight).\n    const panel = this.root.contents().find((c) => c.isActiveContent)?.panel ?? null\n    if (panel !== this.observed) {\n      this.ro?.disconnect()\n      this.observed = panel\n      if (!panel) return\n      const measure = () => {\n        const next = { width: panel.offsetWidth, height: panel.offsetHeight }\n        const prev = this.size()\n        if (!prev || prev.width !== next.width || prev.height !== next.height) this.size.set(next)\n      }\n      measure()\n      this.ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null\n      this.ro?.observe(panel)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.ro?.disconnect()\n    if (this.root.viewportComponent === this) this.root.viewportComponent = undefined\n  }\n\n  get viewportClass(): string {\n    return cn(\n      'origin-top-center bg-popover text-popover-foreground data-[state=open]:motion-safe:animate-in data-[state=closed]:motion-safe:animate-out data-[state=closed]:motion-safe:zoom-out-95 data-[state=open]:motion-safe:zoom-in-90 relative left-[var(--radix-navigation-menu-viewport-left)] mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',\n      this.className,\n    )\n  }\n}\n\n/**\n * Angular port of UIPKGE NavigationMenu with Radix NavigationMenu behaviour: triggers\n * open on hover (delayDuration, then skipDelayDuration for quick moves between\n * triggers) or click, close 150ms after the pointer leaves, and Escape / outside\n * pointer-down / focus leaving the menu dismiss. Content renders into the shared\n * viewport (or inline as a popover with `[viewport]=\"false\"`) with data-motion\n * from-/to-start/end so panels slide in the direction of travel. Arrow keys move\n * between triggers and links, ArrowDown enters an open panel. Use `<nav ui-navigation-menu>`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu, [ui-navigation-menu]',\n  standalone: true,\n  imports: [UiNavigationMenuViewportComponent],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"navigation-menu\"',\n    '[attr.data-viewport]': 'viewport ? \"true\" : \"false\"',\n    '[attr.data-orientation]': 'orientation',\n    '[attr.dir]': 'dir',\n    '[attr.aria-label]': '\"Main\"',\n    '[attr.role]': 'isNav ? null : \"navigation\"',\n    '[class]': 'hostClass',\n    '(focusout)': 'onFocusOut($event)',\n  },\n  template: `<ng-content />\n    @if (viewport) {\n      <ui-navigation-menu-viewport />\n    }`,\n})\nexport class UiNavigationMenuComponent implements OnChanges, OnDestroy {\n  readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly isNav = this.el.tagName === 'NAV'\n  @Input('class') className?: string\n  /** Controlled open item value ('' = closed). */\n  @Input() value?: string\n  @Input() defaultValue = ''\n  @Output() valueChange = new EventEmitter<string>()\n  @Input({ transform: booleanAttribute }) viewport = true\n  @Input() delayDuration = 200\n  @Input() skipDelayDuration = 300\n  @Input() orientation: NavigationMenuOrientation = 'horizontal'\n  @Input() dir: 'ltr' | 'rtl' = 'ltr'\n\n  readonly baseId = uniqueId('navigation-menu')\n  private readonly _value = signal<string | null>(null)\n  readonly previousValue = signal('')\n  readonly items: UiNavigationMenuItemComponent[] = []\n  readonly contents = signal<UiNavigationMenuContentComponent[]>([])\n  viewportComponent?: UiNavigationMenuViewportComponent\n  indicatorComponent?: UiNavigationMenuIndicatorComponent\n  indicatorTrack: HTMLElement | null = null\n\n  private isOpenDelayed = true\n  private openTimer?: ReturnType<typeof setTimeout>\n  private closeTimer?: ReturnType<typeof setTimeout>\n  private skipDelayTimer?: ReturnType<typeof setTimeout>\n  private removeLayer: (() => void) | null = null\n\n  get currentValue(): string {\n    if (this.value !== undefined) return this.value\n    return this._value() ?? this.defaultValue\n  }\n\n  get isOpen(): boolean {\n    return this.currentValue !== ''\n  }\n\n  /** Radix: the viewport keeps showing the last content while it animates closed. */\n  get activeContentValue(): string {\n    return this.isOpen ? this.currentValue : this.previousValue()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const change = changes['value']\n    if (change && !change.firstChange) this.transition(change.previousValue ?? '', change.currentValue ?? '')\n  }\n\n  ngOnDestroy(): void {\n    clearTimeout(this.openTimer)\n    clearTimeout(this.closeTimer)\n    clearTimeout(this.skipDelayTimer)\n    this.removeLayer?.()\n  }\n\n  setValue(next: string): void {\n    const prev = this.currentValue\n    if (next === prev) return\n    if (this.value === undefined) {\n      this.transition(prev, next)\n      this._value.set(next)\n    }\n    clearTimeout(this.skipDelayTimer)\n    if (next !== '') {\n      if (this.skipDelayDuration > 0) this.isOpenDelayed = false\n    } else {\n      this.skipDelayTimer = setTimeout(() => (this.isOpenDelayed = true), this.skipDelayDuration)\n    }\n    this.valueChange.emit(next)\n  }\n\n  /** Keeps leaving content / the closing viewport mounted for their exit animations. */\n  private transition(prev: string, next: string): void {\n    const activeBefore = prev !== '' ? prev : this.previousValue()\n    const activeAfter = next !== '' ? next : prev\n    for (const c of this.contents()) {\n      const v = c.item.value\n      const was = this.viewport ? v === activeBefore : v === prev\n      const now = this.viewport ? v === activeAfter : v === next\n      if (was && !now) c.beginExit()\n    }\n    if (prev !== '' && next === '') {\n      this.viewportComponent?.beginExit()\n      this.indicatorComponent?.beginExit()\n    }\n    this.previousValue.set(prev)\n    if (next !== '' && !this.removeLayer) {\n      this.removeLayer = pushDismissableLayer({\n        contains: (t) => this.containsInteractive(t),\n        onEscape: () => {\n          const item = this.items.find((i) => i.value === this.currentValue)\n          if (item) item.wasEscapeClose = true\n          item?.triggerEl?.focus()\n          this.setValue('')\n        },\n        onPointerDownOutside: () => this.setValue(''),\n      })\n    } else if (next === '') {\n      this.removeLayer?.()\n      this.removeLayer = null\n    }\n  }\n\n  /** Radix onPointerDownOutside: triggers, the viewport and the open content do not dismiss. */\n  private containsInteractive(target: Node): boolean {\n    if (this.items.some((i) => i.triggerEl?.contains(target))) return true\n    if (this.viewportComponent?.viewportRef?.nativeElement.contains(target)) return true\n    return this.contents().some((c) => c.panel?.contains(target))\n  }\n\n  onTriggerEnter(itemValue: string): void {\n    clearTimeout(this.openTimer)\n    if (this.isOpenDelayed) {\n      if (this.currentValue === itemValue) {\n        clearTimeout(this.closeTimer)\n        return\n      }\n      this.openTimer = setTimeout(() => {\n        clearTimeout(this.closeTimer)\n        this.setValue(itemValue)\n      }, this.delayDuration)\n    } else {\n      clearTimeout(this.closeTimer)\n      this.setValue(itemValue)\n    }\n  }\n\n  onTriggerLeave(): void {\n    clearTimeout(this.openTimer)\n    this.startCloseTimer()\n  }\n\n  onContentEnter(): void {\n    clearTimeout(this.closeTimer)\n  }\n\n  onContentLeave(): void {\n    this.startCloseTimer()\n  }\n\n  onItemSelect(itemValue: string): void {\n    this.setValue(this.currentValue === itemValue ? '' : itemValue)\n  }\n\n  private startCloseTimer(): void {\n    clearTimeout(this.closeTimer)\n    this.closeTimer = setTimeout(() => this.setValue(''), 150)\n  }\n\n  /** Radix onFocusOutside: focus moving out of the whole menu closes it. */\n  onFocusOut(event: FocusEvent): void {\n    const to = event.relatedTarget\n    if (this.isOpen && to instanceof Node && !this.el.contains(to)) this.setValue('')\n  }\n\n  registerContent(content: UiNavigationMenuContentComponent): void {\n    this.contents.update((list) => [...list, content])\n  }\n\n  unregisterContent(content: UiNavigationMenuContentComponent): void {\n    this.contents.update((list) => list.filter((c) => c !== content))\n  }\n\n  /** Radix data-motion: the direction content enters from / leaves to when switching items. */\n  motionFor(\n    itemValue: string,\n    last: NavigationMenuMotion | null,\n  ): { motion: NavigationMenuMotion | null; changed: boolean } {\n    const values = this.items.map((i) => i.value)\n    if (this.dir === 'rtl') values.reverse()\n    const index = values.indexOf(this.currentValue)\n    const prevIndex = values.indexOf(this.previousValue())\n    const isSelected = itemValue === this.currentValue\n    const wasSelected = prevIndex === values.indexOf(itemValue)\n    if (!isSelected && !wasSelected) return { motion: last, changed: false }\n    let motion: NavigationMenuMotion | null = null\n    if (index !== prevIndex) {\n      if (isSelected && prevIndex !== -1) motion = index > prevIndex ? 'from-end' : 'from-start'\n      else if (wasSelected && index !== -1) motion = index > prevIndex ? 'to-start' : 'to-end'\n    }\n    return { motion, changed: true }\n  }\n\n  /** Radix FocusGroup: arrows / Home / End move between triggers and links of one group (no looping). */\n  onFocusGroupKeydown(event: KeyboardEvent, current: HTMLElement): void {\n    const arrows = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']\n    if (![...arrows, 'Home', 'End'].includes(event.key)) return\n    const group = current.closest('[data-slot=\"navigation-menu-content\"], [data-slot=\"navigation-menu-list\"]')\n    if (!group) return\n    let candidates = [\n      ...group.querySelectorAll<HTMLElement>(\n        '[data-slot=\"navigation-menu-trigger\"], [data-slot=\"navigation-menu-link\"]',\n      ),\n    ].filter(\n      (n) =>\n        n.closest('[data-slot=\"navigation-menu-content\"], [data-slot=\"navigation-menu-list\"]') === group &&\n        !n.hasAttribute('disabled'),\n    )\n    const prevKey = this.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n    if ([prevKey, 'ArrowUp', 'End'].includes(event.key)) candidates.reverse()\n    if (arrows.includes(event.key)) candidates = candidates.slice(candidates.indexOf(current) + 1)\n    event.preventDefault()\n    setTimeout(() => candidates[0]?.focus())\n  }\n\n  get hostClass(): string {\n    return cn('group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', this.className)\n  }\n}\n\n/**\n * Radix List renders `<div style=\"position: relative\"><ul>`: the host is that\n * relative track (the indicator is positioned in it) and the <ul> carries the\n * classes. Put `<li ui-navigation-menu-item>` children inside.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu-list, [ui-navigation-menu-list]',\n  standalone: true,\n  host: { class: 'relative block' },\n  template: `<ul\n    data-uipkge=\"\"\n    data-slot=\"navigation-menu-list\"\n    [attr.data-orientation]=\"root.orientation\"\n    [class]=\"listClass\"\n  >\n    <ng-content />\n  </ul>`,\n})\nexport class UiNavigationMenuListComponent implements OnDestroy {\n  readonly root = inject(UiNavigationMenuComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  @Input('class') className?: string\n\n  constructor() {\n    this.root.indicatorTrack = this.el\n  }\n\n  ngOnDestroy(): void {\n    if (this.root.indicatorTrack === this.el) this.root.indicatorTrack = null\n  }\n\n  get listClass(): string {\n    return cn('group flex flex-1 list-none items-center justify-center gap-1', this.className)\n  }\n}\n\n/** Use `<li ui-navigation-menu-item>` (Radix renders an <li>). */\n@Directive({\n  selector: 'ui-navigation-menu-item, [ui-navigation-menu-item]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"navigation-menu-item\"',\n    '[class]': 'hostClass',\n  },\n})\nexport class UiNavigationMenuItemComponent implements OnDestroy {\n  readonly root = inject(UiNavigationMenuComponent)\n  @Input('class') className?: string\n  @Input() value: string = uniqueId('navigation-menu-item')\n\n  triggerEl?: HTMLElement\n  content?: UiNavigationMenuContentComponent\n  wasEscapeClose = false\n\n  constructor() {\n    this.root.items.push(this)\n  }\n\n  ngOnDestroy(): void {\n    const i = this.root.items.indexOf(this)\n    if (i >= 0) this.root.items.splice(i, 1)\n  }\n\n  get isOpen(): boolean {\n    return this.root.currentValue === this.value\n  }\n\n  get triggerId(): string {\n    return `${this.root.baseId}-trigger-${this.value}`\n  }\n\n  get contentId(): string {\n    return `${this.root.baseId}-content-${this.value}`\n  }\n\n  /** Focus the first tabbable element of this item's open panel (Radix onEntryKeyDown). */\n  focusContent(fromEnd = false): boolean {\n    const items = tabbables(this.content?.panel)\n    const target = fromEnd ? items[items.length - 1] : items[0]\n    target?.focus()\n    return !!target\n  }\n\n  get hostClass(): string {\n    return cn('relative', this.className)\n  }\n}\n\n/** Use `<button ui-navigation-menu-trigger>` (Radix renders a button). */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu-trigger, [ui-navigation-menu-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"navigation-menu-trigger\"',\n    '[attr.id]': 'item.triggerId',\n    '[attr.type]': 'isNativeButton ? \"button\" : null',\n    '[attr.role]': 'isNativeButton ? null : \"button\"',\n    '[attr.tabindex]': 'isNativeButton ? null : 0',\n    '[attr.disabled]': 'isNativeButton && disabled ? \"\" : null',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.data-state]': 'item.isOpen ? \"open\" : \"closed\"',\n    '[attr.aria-expanded]': 'item.isOpen',\n    '[attr.aria-controls]': 'item.contentId',\n    '[class]': 'hostClass',\n    '(pointerenter)': 'onPointerEnter()',\n    '(pointermove)': 'onPointerMove($event)',\n    '(pointerleave)': 'onPointerLeave($event)',\n    '(click)': 'onClick()',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `<ng-content /><svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class=\"lucide lucide-chevron-down relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>`,\n})\nexport class UiNavigationMenuTriggerComponent {\n  readonly item = inject(UiNavigationMenuItemComponent)\n  private readonly root = this.item.root\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly isNativeButton = this.el.tagName === 'BUTTON'\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) disabled = false\n\n  private wasClickClose = false\n  private hasPointerMoveOpened = false\n\n  constructor() {\n    this.item.triggerEl = this.el\n  }\n\n  onPointerEnter(): void {\n    this.wasClickClose = false\n    this.item.wasEscapeClose = false\n  }\n\n  onPointerMove(event: PointerEvent): void {\n    if (!isMouse(event) || this.disabled || this.wasClickClose || this.item.wasEscapeClose || this.hasPointerMoveOpened)\n      return\n    this.root.onTriggerEnter(this.item.value)\n    this.hasPointerMoveOpened = true\n  }\n\n  onPointerLeave(event: PointerEvent): void {\n    if (!isMouse(event) || this.disabled) return\n    this.root.onTriggerLeave()\n    this.hasPointerMoveOpened = false\n  }\n\n  onClick(): void {\n    if (this.disabled) return\n    const wasOpen = this.item.isOpen\n    this.root.onItemSelect(this.item.value)\n    this.wasClickClose = wasOpen\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (!this.isNativeButton && (event.key === 'Enter' || event.key === ' ')) {\n      event.preventDefault()\n      this.onClick()\n      return\n    }\n    const entryKey =\n      this.root.orientation === 'horizontal' ? 'ArrowDown' : this.root.dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n    if (this.item.isOpen && event.key === entryKey) {\n      this.item.focusContent()\n      event.preventDefault()\n      return\n    }\n    // Radix focus proxy: Tab from an open trigger moves into its panel.\n    if (this.item.isOpen && event.key === 'Tab' && !event.shiftKey && this.item.focusContent()) {\n      event.preventDefault()\n      return\n    }\n    this.root.onFocusGroupKeydown(event, this.el)\n  }\n\n  get hostClass(): string {\n    return cn(navigationMenuTriggerStyle(), 'group', this.className)\n  }\n}\n\n/**\n * Content for one item. The panel is an <ng-template>: the viewport renders it while\n * the item is active (or sliding out), or -- with `[viewport]=\"false\"` -- it renders in\n * place after this (display: contents) host as a popover under the trigger.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu-content, [ui-navigation-menu-content]',\n  standalone: true,\n  imports: [UiRenderTemplateDirective, UiNavigationMenuRefDirective],\n  host: { class: 'contents' },\n  template: `<ng-template #panelTemplate>\n      <div\n        [uiNavigationMenuRef]=\"setPanel\"\n        data-uipkge=\"\"\n        data-slot=\"navigation-menu-content\"\n        [attr.id]=\"item.contentId\"\n        [attr.aria-labelledby]=\"item.triggerId\"\n        [attr.data-motion]=\"motion\"\n        [attr.data-state]=\"item.isOpen ? 'open' : 'closed'\"\n        [attr.data-orientation]=\"root.orientation\"\n        [class]=\"contentClass\"\n        (pointerenter)=\"onPointerEnter()\"\n        (pointerleave)=\"onPointerLeave($event)\"\n        (keydown)=\"onKeydown($event)\"\n        (click)=\"onClick($event)\"\n      >\n        <ng-content />\n      </div>\n    </ng-template>\n    @if (!root.viewport && present) {\n      <ng-container [uiRenderTemplate]=\"panelTemplate\" />\n    }`,\n})\nexport class UiNavigationMenuContentComponent implements AfterViewChecked, OnDestroy {\n  readonly item = inject(UiNavigationMenuItemComponent)\n  readonly root = this.item.root\n  @Input('class') className?: string\n  @ViewChild('panelTemplate', { static: true }) template!: TemplateRef<unknown>\n  /** The rendered panel element (in viewport mode it lives inside the viewport). */\n  panel: HTMLElement | undefined\n  readonly setPanel = (el: HTMLElement | null) => (this.panel = el ?? undefined)\n\n  private readonly exiting = signal(false)\n  private lastMotion: NavigationMenuMotion | null = null\n  private wasActive = false\n\n  constructor() {\n    this.item.content = this\n    this.root.registerContent(this)\n  }\n\n  /** The content the viewport shows (and sizes itself to). */\n  get isActiveContent(): boolean {\n    return this.root.viewport ? this.item.value === this.root.activeContentValue : this.item.isOpen\n  }\n\n  get present(): boolean {\n    return this.isActiveContent || this.exiting()\n  }\n\n  get motion(): NavigationMenuMotion | null {\n    const { motion, changed } = this.root.motionFor(this.item.value, this.lastMotion)\n    if (changed) this.lastMotion = motion\n    return motion\n  }\n\n  beginExit(): void {\n    this.exiting.set(true)\n  }\n\n  /** Radix Presence: unmount once the exit animation (slide-out / zoom-out) has finished. */\n  ngAfterViewChecked(): void {\n    const active = this.isActiveContent\n    if (active === this.wasActive) return\n    this.wasActive = active\n    if (active) {\n      this.exiting.set(false)\n      return\n    }\n    const panel = this.panel\n    void afterExitAnimation(panel).then(() => {\n      if (!this.isActiveContent) this.exiting.set(false)\n    })\n  }\n\n  ngOnDestroy(): void {\n    this.root.unregisterContent(this)\n    if (this.item.content === this) this.item.content = undefined\n  }\n\n  onPointerEnter(): void {\n    if (!this.root.viewport) this.root.onContentEnter()\n  }\n\n  onPointerLeave(event: PointerEvent): void {\n    if (!this.root.viewport && isMouse(event)) this.root.onContentLeave()\n  }\n\n  /** Tab walks the panel, then leaves it next to the trigger (Radix focus proxy). */\n  onKeydown(event: KeyboardEvent): void {\n    const panel = event.currentTarget as HTMLElement\n    if (event.key === 'Tab' && !event.metaKey && !event.ctrlKey && !event.altKey) {\n      const candidates = tabbables(panel)\n      const index = candidates.indexOf(document.activeElement as HTMLElement)\n      const next = event.shiftKey ? candidates.slice(0, index).reverse() : candidates.slice(index + 1)\n      if (next[0]) {\n        next[0].focus()\n        event.preventDefault()\n      } else if (this.item.triggerEl) {\n        this.item.triggerEl.focus()\n        // Backwards lands on the trigger; forwards lets the browser Tab on from it.\n        if (event.shiftKey) event.preventDefault()\n      }\n      return\n    }\n    const target = event.target as HTMLElement\n    if (target.matches('[data-slot=\"navigation-menu-link\"]')) return\n    this.root.onFocusGroupKeydown(event, target)\n  }\n\n  /** Radix Link select: activating a NavigationMenuLink inside content closes the menu. */\n  onClick(event: MouseEvent): void {\n    const link = (event.target as HTMLElement).closest('[data-slot=\"navigation-menu-link\"]')\n    if (link && !event.defaultPrevented && !event.metaKey && !event.ctrlKey) this.root.setValue('')\n  }\n\n  get contentClass(): string {\n    return cn(navigationMenuContentVariants(), CONTENT_POPOVER_CLASSES, this.className)\n  }\n}\n\n/** Use `<a ui-navigation-menu-link href=\"...\">` (Radix renders an anchor). */\n@Directive({\n  selector: 'ui-navigation-menu-link, [ui-navigation-menu-link]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"navigation-menu-link\"',\n    '[attr.data-active]': 'active ? \"\" : null',\n    '[attr.aria-current]': 'active ? \"page\" : null',\n    '[class]': 'hostClass',\n    '(click)': 'onClick($event)',\n    '(keydown)': 'onKeydown($event)',\n  },\n})\nexport class UiNavigationMenuLinkComponent {\n  private readonly root = inject(UiNavigationMenuComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  @Input({ transform: booleanAttribute }) active = false\n  @Input('class') className?: string\n  /** Radix Link `onSelect`: `preventDefault()` keeps the menu open (meta-click never closes). */\n  @Output() select = new EventEmitter<Event>()\n\n  /** Radix: activating a link selects it, then dismisses the menu unless prevented. */\n  onClick(event: MouseEvent): void {\n    const selectEvent = new Event('select', { cancelable: true })\n    this.select.emit(selectEvent)\n    if (!selectEvent.defaultPrevented && !event.metaKey) this.root.setValue('')\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    this.root.onFocusGroupKeydown(event, this.el)\n  }\n\n  get hostClass(): string {\n    return cn(\n      \"data-active:focus:bg-accent data-active:hover:bg-accent data-active:bg-accent/50 data-active:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4\",\n      this.className,\n    )\n  }\n}\n\n/**\n * Radix Indicator: portalled into the list's relative track and slid under the open\n * trigger (offsetLeft / offsetWidth, ResizeObserver-tracked). Hidden until a menu first opens.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-navigation-menu-indicator, [ui-navigation-menu-indicator]',\n  standalone: true,\n  imports: [UiRenderTemplateDirective, UiNavigationMenuRefDirective],\n  host: { class: 'hidden' },\n  template: `<ng-template #indicatorTemplate>\n      <div\n        [uiNavigationMenuRef]=\"setIndicator\"\n        data-uipkge=\"\"\n        data-slot=\"navigation-menu-indicator\"\n        aria-hidden=\"true\"\n        [attr.data-state]=\"root.isOpen ? 'visible' : 'hidden'\"\n        [attr.data-orientation]=\"root.orientation\"\n        [class]=\"hostClass\"\n        [style]=\"indicatorStyle\"\n      >\n        <div class=\"bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\"></div>\n      </div>\n    </ng-template>\n    @if (present) {\n      <ng-container [uiRenderTemplate]=\"indicatorTemplate\" />\n    }`,\n})\nexport class UiNavigationMenuIndicatorComponent implements AfterViewChecked, OnDestroy {\n  readonly root = inject(UiNavigationMenuComponent)\n  @Input('class') className?: string\n  private indicatorEl: HTMLElement | null = null\n  /** Portal: the rendered indicator moves into the list track, beside the <ul> (Radix createPortal). */\n  readonly setIndicator = (el: HTMLElement | null) => {\n    this.indicatorEl = el\n    if (el) this.root.indicatorTrack?.appendChild(el)\n  }\n\n  private readonly exiting = signal(false)\n  private readonly position = signal<{ size: number; offset: number } | null>(null)\n  private wasOpen = false\n  private activeTrigger: HTMLElement | null = null\n  private ro: ResizeObserver | null = null\n\n  constructor() {\n    this.root.indicatorComponent = this\n  }\n\n  get present(): boolean {\n    return (this.root.isOpen || this.exiting()) && this.position() !== null\n  }\n\n  beginExit(): void {\n    this.exiting.set(true)\n  }\n\n  get indicatorStyle(): Record<string, string> {\n    const p = this.position()\n    if (!p) return {}\n    return this.root.orientation === 'horizontal'\n      ? { position: 'absolute', left: '0', width: `${p.size}px`, transform: `translateX(${p.offset}px)` }\n      : { position: 'absolute', top: '0', height: `${p.size}px`, transform: `translateY(${p.offset}px)` }\n  }\n\n  ngAfterViewChecked(): void {\n    const open = this.root.isOpen\n    if (open !== this.wasOpen) {\n      this.wasOpen = open\n      if (open) this.exiting.set(false)\n      else {\n        void afterExitAnimation(this.indicatorEl).then(() => {\n          if (!this.root.isOpen) this.exiting.set(false)\n        })\n      }\n    }\n    // Follow the open trigger (Radix keeps the last one while hidden).\n    const trigger = this.root.items.find((i) => i.value === this.root.currentValue)?.triggerEl ?? null\n    if (trigger && trigger !== this.activeTrigger) {\n      this.activeTrigger = trigger\n      this.ro?.disconnect()\n      const update = () => {\n        const horizontal = this.root.orientation === 'horizontal'\n        const next = {\n          size: horizontal ? trigger.offsetWidth : trigger.offsetHeight,\n          offset: horizontal ? trigger.offsetLeft : trigger.offsetTop,\n        }\n        const prev = this.position()\n        if (!prev || prev.size !== next.size || prev.offset !== next.offset) this.position.set(next)\n      }\n      update()\n      this.ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(update) : null\n      this.ro?.observe(trigger)\n      if (this.root.indicatorTrack) this.ro?.observe(this.root.indicatorTrack)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.ro?.disconnect()\n    this.indicatorEl?.remove()\n    if (this.root.indicatorComponent === this) this.root.indicatorComponent = undefined\n  }\n\n  get hostClass(): string {\n    return cn(\n      'data-[state=visible]:motion-safe:animate-in data-[state=hidden]:motion-safe:animate-out data-[state=hidden]:motion-safe:fade-out data-[state=visible]:motion-safe:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',\n      this.className,\n    )\n  }\n}\n\nexport { navigationMenuTriggerStyle } from './navigation-menu.variants'\nexport { navigationMenuContentVariants } from './navigation-menu-content.variants'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/navigation-menu/navigation-menu.component.ts"
    },
    {
      "path": "packages/registry-angular/components/navigation-menu/navigation-menu.variants.ts",
      "content": "import { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const navigationMenuTriggerStyle = cva(\n  'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1',\n)\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/navigation-menu/navigation-menu.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/navigation-menu/navigation-menu-content.variants.ts",
      "content": "import { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n *\n * Matches the React NavigationMenuContent motion + layout string (slide distance 52).\n */\nexport const navigationMenuContentVariants = cva(\n  'data-[motion^=from-]:motion-safe:animate-in data-[motion^=to-]:motion-safe:animate-out data-[motion^=from-]:motion-safe:fade-in data-[motion^=to-]:motion-safe:fade-out data-[motion=from-end]:motion-safe:slide-in-from-right-52 data-[motion=from-start]:motion-safe:slide-in-from-left-52 data-[motion=to-end]:motion-safe:slide-out-to-right-52 data-[motion=to-start]:motion-safe:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',\n)\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/navigation-menu/navigation-menu-content.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/navigation-menu/index.ts",
      "content": "export {\n  UiNavigationMenuComponent,\n  UiNavigationMenuListComponent,\n  UiNavigationMenuItemComponent,\n  UiNavigationMenuTriggerComponent,\n  UiNavigationMenuContentComponent,\n  UiNavigationMenuLinkComponent,\n  UiNavigationMenuViewportComponent,\n  UiNavigationMenuIndicatorComponent,\n  type NavigationMenuOrientation,\n  type NavigationMenuMotion,\n} from './navigation-menu.component'\nexport { navigationMenuTriggerStyle } from './navigation-menu.variants'\nexport { navigationMenuContentVariants } from './navigation-menu-content.variants'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/navigation-menu/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Top-of-page horizontal navigation with hover/click triggered megamenus. Use for marketing sites and product navs that need rich content — featured links, mini-cards, and submenu columns. Radix NavigationMenu behaviour: hover delays, a shared viewport that sizes to the open panel, directional slide motion, and keyboard navigation.",
  "categories": [
    "navigation"
  ]
}