{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "menubar",
  "title": "Menubar",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/menubar/menubar.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  TemplateRef,\n  ViewChild,\n  ViewContainerRef,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  BodyPortal,\n  afterExitAnimation,\n  autoPlace,\n  computePlacement,\n  pushDismissableLayer,\n  uniqueId,\n  type PopperAlign,\n  type PopperSide,\n  type PlaceOptions,\n} from '@/ui/popper/popper'\n\nexport type MenubarItemVariant = 'default' | 'destructive'\nexport type MenubarCheckedState = boolean | 'indeterminate'\nexport type MenubarDir = 'ltr' | 'rtl'\nexport type MenubarSide = PopperSide\nexport type MenubarAlign = PopperAlign\n\n/**\n * autoPlace() positions on the next animation frame; place once synchronously too so the\n * panel never paints (or catches a fast click's pointerup) at the viewport origin.\n */\nfunction placeAndTrack(anchor: Element, panel: HTMLElement, options: () => PlaceOptions, prefix: string): () => void {\n  const stop = autoPlace(anchor, panel, options, prefix)\n  const p = computePlacement(\n    anchor.getBoundingClientRect(),\n    { width: panel.offsetWidth, height: panel.offsetHeight },\n    options(),\n  )\n  panel.style.left = `${Math.round(p.x)}px`\n  panel.style.top = `${Math.round(p.y)}px`\n  panel.setAttribute('data-side', p.side)\n  panel.setAttribute('data-align', p.align)\n  return stop\n}\n\n/*\n * Angular port of UIPKGE Menubar with Radix Menubar behaviour: a row of menus whose\n * triggers share one roving tab stop (ArrowLeft / ArrowRight / Home / End between them).\n * Pressing a trigger (or Enter / Space / ArrowDown) opens its menu in a body portal under\n * it; while one menu is open, hovering another trigger switches to it and ArrowLeft /\n * ArrowRight inside a menu move to the neighbouring menu. Items, checkbox / radio items,\n * labels, separators, shortcuts and hover / ArrowRight submenus behave like the Radix\n * menu (roving focus, typeahead, pointer grace area). Escape and outside presses close;\n * closing from inside hands focus back to the trigger. Class strings match React.\n */\n\n// ---------------------------------------------------------------------------------------\n// Shared menu machinery (Radix Menu): roving focus, typeahead, pointer grace area.\n// ---------------------------------------------------------------------------------------\n\ninterface Point {\n  x: number\n  y: number\n}\ninterface GraceIntent {\n  area: Point[]\n  side: 'left' | 'right'\n}\n\nfunction isPointInPolygon(point: Point, polygon: Point[]): boolean {\n  let inside = false\n  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n    const a = polygon[i]!\n    const b = polygon[j]!\n    if (a.y > point.y !== b.y > point.y && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x)\n      inside = !inside\n  }\n  return inside\n}\n\nconst isMouse = (event: PointerEvent) => event.pointerType === 'mouse'\nconst FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home']\nconst LAST_KEYS = ['ArrowUp', 'PageDown', 'End']\n\n/** Keyboard + pointer state of one menu panel (root content or a submenu). */\nclass MenuPanel {\n  el?: HTMLElement\n  openSub?: UiMenubarSubComponent\n  /** Typeahead buffer; while non-empty a Space extends the search instead of selecting. */\n  search = ''\n  private searchTimer: ReturnType<typeof setTimeout> | undefined\n  private lastX = 0\n  private pointerDir: 'left' | 'right' = 'right'\n  private grace: GraceIntent | null = null\n  private graceTimer: ReturnType<typeof setTimeout> | undefined\n\n  constructor(private readonly loop: () => boolean) {}\n\n  items(): HTMLElement[] {\n    if (!this.el) return []\n    return [...this.el.querySelectorAll<HTMLElement>('[role^=\"menuitem\"]')].filter(\n      (el) => !el.hasAttribute('data-disabled'),\n    )\n  }\n\n  focusFirst(): void {\n    this.items()[0]?.focus({ preventScroll: true })\n  }\n\n  focusPanel(): void {\n    this.el?.focus({ preventScroll: true })\n  }\n\n  keydown(event: KeyboardEvent): void {\n    const panel = this.el\n    const target = event.target as HTMLElement\n    if (!panel) return\n    if (target.closest('[role=\"menu\"]') === panel) {\n      if (event.key === 'Tab') event.preventDefault()\n      if (!event.ctrlKey && !event.altKey && !event.metaKey && event.key.length === 1) this.typeahead(event.key)\n    }\n    const items = this.items()\n    if (target === panel) {\n      // Menu itself focused (pointer open): the first arrow press enters the list.\n      if (FIRST_KEYS.includes(event.key)) items[0]?.focus({ preventScroll: true })\n      else if (LAST_KEYS.includes(event.key)) items[items.length - 1]?.focus({ preventScroll: true })\n      else return\n      event.preventDefault()\n      return\n    }\n    if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return\n    const current = items.indexOf(target)\n    if (current < 0) return\n    let next: number\n    if (event.key === 'ArrowDown') next = current + 1\n    else if (event.key === 'ArrowUp') next = current - 1\n    else if (event.key === 'Home' || event.key === 'PageUp') next = 0\n    else if (event.key === 'End' || event.key === 'PageDown') next = items.length - 1\n    else return\n    event.preventDefault()\n    if (this.loop()) next = (next + items.length) % items.length\n    else next = Math.min(Math.max(next, 0), items.length - 1)\n    items[next]?.focus({ preventScroll: true })\n  }\n\n  /** Radix typeahead: accumulate for 1s, a repeated single character cycles matches. */\n  private typeahead(key: string): void {\n    const search = this.search + key\n    const items = this.items()\n    const text = (el: HTMLElement) => (el.getAttribute('data-text-value') ?? el.textContent ?? '').trim()\n    const current = items.find((el) => el === document.activeElement)\n    const repeated = search.length > 1 && [...search].every((c) => c === search[0])\n    const needle = (repeated ? search[0]! : search).toLowerCase()\n    const start = current ? items.indexOf(current) : 0\n    let candidates = [...items.slice(start), ...items.slice(0, start)]\n    if (needle.length === 1) candidates = candidates.filter((el) => el !== current)\n    const match = candidates.find((el) => text(el).toLowerCase().startsWith(needle))\n    this.search = search\n    clearTimeout(this.searchTimer)\n    this.searchTimer = setTimeout(() => (this.search = ''), 1000)\n    if (match && match !== current) setTimeout(() => match.focus({ preventScroll: true }))\n  }\n\n  pointerMove(event: PointerEvent): void {\n    if (!isMouse(event) || !this.el?.contains(event.target as Node) || event.clientX === this.lastX) return\n    this.pointerDir = event.clientX > this.lastX ? 'right' : 'left'\n    this.lastX = event.clientX\n  }\n\n  /** True while the pointer travels from a sub-trigger toward its open submenu. */\n  isMovingToSub(event: PointerEvent): boolean {\n    const g = this.grace\n    return !!g && this.pointerDir === g.side && isPointInPolygon({ x: event.clientX, y: event.clientY }, g.area)\n  }\n\n  setGrace(intent: GraceIntent | null): void {\n    this.grace = intent\n    clearTimeout(this.graceTimer)\n    if (intent) this.graceTimer = setTimeout(() => (this.grace = null), 300)\n  }\n\n  /** Radix `onItemLeave`: the pointer left an item, so the menu itself takes focus. */\n  itemLeave(event: PointerEvent): void {\n    if (this.isMovingToSub(event)) return\n    this.focusPanel()\n  }\n\n  /** Focus landing anywhere in this panel except the open sub's trigger closes that sub. */\n  focusMoved(target: EventTarget | null): void {\n    const sub = this.openSub\n    if (sub && target !== sub.trigger) sub.setOpen(false)\n  }\n\n  destroy(): void {\n    clearTimeout(this.searchTimer)\n    clearTimeout(this.graceTimer)\n    this.search = ''\n    this.grace = null\n  }\n}\n\n// ---------------------------------------------------------------------------------------\n// Root, menu, trigger, content\n// ---------------------------------------------------------------------------------------\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar, [ui-menubar]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"menubar\"',\n    '[attr.data-orientation]': '\"horizontal\"',\n    '[attr.tabindex]': 'tabbingBackOut || !menus.size ? -1 : 0',\n    '[class]': 'hostClass',\n    '(mousedown)': 'clickFocus = true',\n    '(focusin)': 'onFocus($event)',\n    '(focusout)': 'onFocusOut()',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarComponent {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  @Input('class') className?: string\n  /** The open menu's `value` ('' when every menu is closed). */\n  @Input() value?: string\n  @Input() defaultValue = ''\n  @Input({ transform: booleanAttribute }) loop = true\n  @Input() dir: MenubarDir = 'ltr'\n  @Output() valueChange = new EventEmitter<string>()\n\n  private readonly _value = signal<string | null>(null)\n  /** Roving tab stop: the trigger Tab lands on (the last focused / opened one). */\n  readonly tabStop = signal<string | null>(null)\n  readonly menus = new Map<HTMLElement, UiMenubarMenuComponent>()\n  tabbingBackOut = false\n  clickFocus = false\n\n  get currentValue(): string {\n    return this.value ?? this._value() ?? this.defaultValue\n  }\n\n  get hostClass(): string {\n    return cn('bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs', this.className)\n  }\n\n  setValue(value: string): void {\n    const previous = this.currentValue\n    if (value === previous) return\n    this._value.set(value)\n    this.valueChange.emit(value)\n    for (const menu of this.menus.values()) if (menu.value === previous || menu.value === value) menu.sync()\n  }\n\n  /** Radix `onMenuOpen`: open this menu (closing any other) and make its trigger the tab stop. */\n  openMenu(value: string): void {\n    this.setValue(value)\n    this.tabStop.set(value)\n  }\n\n  closeMenus(): void {\n    this.setValue('')\n  }\n\n  /** Triggers in DOM order (the roving-focus / ArrowLeft-Right order). */\n  triggers(): { el: HTMLElement; menu: UiMenubarMenuComponent }[] {\n    return [...this.el.nativeElement.querySelectorAll<HTMLElement>('[data-slot=\"menubar-trigger\"]')]\n      .map((el) => ({ el, menu: this.menus.get(el)! }))\n      .filter((t) => !!t.menu)\n  }\n\n  enabledTriggers(): { el: HTMLElement; menu: UiMenubarMenuComponent }[] {\n    return this.triggers().filter((t) => !t.el.hasAttribute('data-disabled'))\n  }\n\n  /** Radix RovingFocusGroup entry: tabbing onto the bar lands on the tab-stop trigger. */\n  onFocus(event: FocusEvent): void {\n    const isKeyboardFocus = !this.clickFocus\n    this.clickFocus = false\n    if (event.target !== this.el.nativeElement || !isKeyboardFocus) return\n    const items = this.enabledTriggers()\n    const target = items.find((t) => t.menu.value === this.tabStop()) ?? items[0]\n    target?.el.focus()\n  }\n\n  /** Arrow / Home / End between triggers (horizontal roving focus). */\n  moveFocus(from: HTMLElement, key: string): boolean {\n    const items = this.enabledTriggers().map((t) => t.el)\n    const i = items.indexOf(from)\n    const prev = this.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n    const next = this.dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n    let target: number\n    if (key === next) target = i + 1\n    else if (key === prev) target = i - 1\n    else if (key === 'Home' || key === 'PageUp') target = 0\n    else if (key === 'End' || key === 'PageDown') target = items.length - 1\n    else return false\n    if (this.loop) target = (target + items.length) % items.length\n    else target = Math.min(Math.max(target, 0), items.length - 1)\n    items[target]?.focus()\n    return true\n  }\n\n  onFocusOut(): void {\n    this.tabbingBackOut = false\n    this.el.nativeElement.setAttribute('tabindex', this.menus.size ? '0' : '-1')\n  }\n\n  /** Shift+Tab out of the bar must not land on the bar itself (Radix `isTabbingBackOut`). */\n  tabBackOut(): void {\n    this.tabbingBackOut = true\n    this.el.nativeElement.setAttribute('tabindex', '-1')\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-menu, [ui-menubar-menu]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-menu\"',\n    '[attr.data-uipkge]': '\"\"',\n    // Radix Menu renders no element: keep the wrapper out of layout.\n    class: 'contents',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarMenuComponent implements OnInit {\n  readonly bar = inject(UiMenubarComponent)\n  /** Identifies the menu in the bar's `value`; auto-generated when not set. */\n  @Input() value = ''\n  readonly triggerId = uniqueId('menubar-trigger')\n  readonly contentId = uniqueId('menubar-content')\n  trigger?: HTMLElement\n  content?: UiMenubarContentComponent\n  /** Radix `wasKeyboardTriggerOpenRef`: Enter / Space / ArrowDown on the trigger focus the first item. */\n  wasKeyboardTriggerOpen = false\n\n  ngOnInit(): void {\n    if (!this.value) this.value = uniqueId('menubar-menu')\n  }\n\n  get dir(): MenubarDir {\n    return this.bar.dir\n  }\n\n  get isOpen(): boolean {\n    return !!this.value && this.bar.currentValue === this.value\n  }\n\n  /** Item family API (select closes, Escape closes). */\n  setOpen(value: boolean): void {\n    if (value) this.bar.openMenu(this.value)\n    else if (this.isOpen) this.bar.closeMenus()\n  }\n\n  sync(): void {\n    if (!this.isOpen) this.wasKeyboardTriggerOpen = false\n    this.content?.sync()\n  }\n}\n\n/** A directive, so it sits on any `<button>` (Radix renders one) and keeps its projected label. */\n@Directive({\n  selector: 'ui-menubar-trigger, [ui-menubar-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'dataSlot',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.type]': 'isButton ? \"button\" : null',\n    '[attr.role]': '\"menuitem\"',\n    '[attr.id]': 'menu.triggerId',\n    '[attr.aria-haspopup]': '\"menu\"',\n    '[attr.aria-expanded]': 'menu.isOpen',\n    '[attr.aria-controls]': 'menu.isOpen ? menu.contentId : null',\n    '[attr.data-highlighted]': 'focused ? \"\" : null',\n    '[attr.data-state]': 'menu.isOpen ? \"open\" : \"closed\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.disabled]': 'disabled && isButton ? \"\" : null',\n    '[attr.data-orientation]': '\"horizontal\"',\n    '[attr.tabindex]': 'menu.bar.tabStop() === menu.value ? 0 : -1',\n    '[class]': 'hostClass',\n    '(pointerdown)': 'onPointerDown($event)',\n    '(pointerenter)': 'onPointerEnter()',\n    '(mousedown)': 'onMouseDown($event)',\n    '(keydown)': 'onKeydown($event)',\n    '(focus)': 'onFocus()',\n    '(blur)': 'focused = false',\n  },\n})\nexport class UiMenubarTriggerComponent implements OnDestroy {\n  // Radix Slot: the trigger's data-slot overrides a wrapped component's own (ui-button's\n  // \"button\"), but a data-slot written on the element itself wins.\n  readonly dataSlot: string = inject(ElementRef).nativeElement.getAttribute('data-slot') ?? 'menubar-trigger'\n  readonly menu = inject(UiMenubarMenuComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) disabled = false\n  focused = false\n  readonly isButton = this.el.nativeElement.tagName === 'BUTTON'\n\n  constructor() {\n    this.menu.trigger = this.el.nativeElement\n    this.menu.bar.menus.set(this.el.nativeElement, this.menu)\n  }\n\n  get hostClass(): string {\n    return cn(\n      'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none',\n      this.className,\n    )\n  }\n\n  /** Left press toggles (ctrl+click is a macOS right-click, so it is ignored). */\n  onPointerDown(event: PointerEvent): void {\n    if (this.disabled || event.button !== 0 || event.ctrlKey) return\n    if (this.menu.isOpen) {\n      // Radix: the open menu's dismissable layer closes it on a press of its own trigger.\n      this.menu.content?.markInteractedOutside()\n      this.menu.setOpen(false)\n      return\n    }\n    this.menu.bar.openMenu(this.menu.value)\n    // Keep focus off the trigger so the opening menu can take it.\n    event.preventDefault()\n  }\n\n  /** While any menu is open, hovering another trigger switches to its menu. */\n  onPointerEnter(): void {\n    if (this.disabled || !this.menu.bar.currentValue || this.menu.isOpen) return\n    this.menu.bar.openMenu(this.menu.value)\n    this.el.nativeElement.focus()\n  }\n\n  /** Radix RovingFocusGroup item: a non-focusable (disabled) trigger never takes focus. */\n  onMouseDown(event: MouseEvent): void {\n    if (this.disabled) event.preventDefault()\n  }\n\n  onFocus(): void {\n    this.focused = true\n    this.menu.bar.tabStop.set(this.menu.value)\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (event.key === 'Tab' && event.shiftKey) return this.menu.bar.tabBackOut()\n    if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {\n      event.preventDefault()\n      // Set before opening: the menu reads it to decide whether the first item takes focus.\n      this.menu.wasKeyboardTriggerOpen = true\n      if (event.key === 'ArrowDown' || !this.menu.isOpen) this.menu.bar.openMenu(this.menu.value)\n      else this.menu.setOpen(false)\n      return\n    }\n    if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return\n    if (this.menu.bar.moveFocus(this.el.nativeElement, event.key)) event.preventDefault()\n  }\n\n  ngOnDestroy(): void {\n    this.menu.bar.menus.delete(this.el.nativeElement)\n  }\n}\n\nexport const MENUBAR_CONTENT_CLASS =\n  'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-content, [ui-menubar-content]',\n  standalone: true,\n  // The host stays where it is declared (no box); the menu itself renders in a body portal.\n  host: { class: 'hidden' },\n  template: `\n    <ng-template #panelTpl>\n      <div\n        [id]=\"menu.contentId\"\n        [attr.aria-labelledby]=\"menu.triggerId\"\n        role=\"menu\"\n        aria-orientation=\"vertical\"\n        data-orientation=\"vertical\"\n        tabindex=\"-1\"\n        data-radix-menubar-content=\"\"\n        data-slot=\"menubar-content\"\n        data-uipkge=\"\"\n        [attr.dir]=\"menu.dir\"\n        [attr.data-state]=\"state()\"\n        [class]=\"panelClass\"\n        (keydown)=\"onKeydown($event)\"\n        (pointermove)=\"panel.pointerMove($event)\"\n        (focus)=\"panel.focusMoved($event.target)\"\n      >\n        <ng-content />\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiMenubarContentComponent implements OnDestroy {\n  readonly menu = inject(UiMenubarMenuComponent)\n  private readonly portal = new BodyPortal(inject(ViewContainerRef))\n\n  @Input('class') className?: string\n  @Input() side: MenubarSide = 'bottom'\n  @Input() align: MenubarAlign = 'start'\n  @Input() sideOffset = 8\n  @Input() alignOffset = -4\n  @Input({ transform: booleanAttribute }) avoidCollisions = true\n  @Input() collisionPadding = 0\n  @Input({ transform: booleanAttribute }) loop = false\n  /** Radix `onCloseAutoFocus`: `preventDefault()` keeps focus where it is after closing. */\n  @Output() closeAutoFocus = new EventEmitter<Event>()\n\n  @ViewChild('panelTpl', { static: true }) panelTpl!: TemplateRef<unknown>\n  readonly panel = new MenuPanel(() => this.loop)\n  readonly state = signal<'open' | 'closed'>('closed')\n  private cleanups: (() => void)[] = []\n  private interactedOutside = false\n\n  constructor() {\n    this.menu.content = this\n  }\n\n  get panelEl(): HTMLElement | undefined {\n    return this.panel.el\n  }\n\n  get panelClass(): string {\n    return cn(MENUBAR_CONTENT_CLASS, this.className)\n  }\n\n  markInteractedOutside(): void {\n    this.interactedOutside = true\n  }\n\n  /** Mirrors the bar's open value into the portal (called by the bar on every change). */\n  sync(): void {\n    if (this.menu.isOpen && (!this.portal.attached || this.state() === 'closed')) {\n      if (this.portal.attached) this.portal.detach()\n      this.show()\n    } else if (!this.menu.isOpen && this.portal.attached && this.state() === 'open') void this.hide()\n  }\n\n  private show(): void {\n    const trigger = this.menu.trigger\n    if (!trigger) return\n    this.interactedOutside = false\n    this.state.set('open')\n    const panel = this.portal.attach(this.panelTpl).firstElementChild as HTMLElement\n    this.panel.el = panel\n    this.cleanups.push(\n      placeAndTrack(\n        trigger,\n        panel,\n        () => ({\n          side: this.side,\n          align: this.align,\n          sideOffset: this.sideOffset,\n          alignOffset: this.alignOffset,\n          collisionPadding: this.collisionPadding,\n          avoidCollisions: this.avoidCollisions,\n        }),\n        'menubar',\n      ),\n      pushDismissableLayer({\n        contains: (t) => panel.contains(t) || trigger.contains(t) || !!this.panel.openSub?.containsTarget(t),\n        onEscape: () => this.menu.setOpen(false),\n        onPointerDownOutside: () => {\n          this.interactedOutside = true\n          this.menu.setOpen(false)\n        },\n      }),\n      () => this.panel.destroy(),\n    )\n    const keyboard = this.menu.wasKeyboardTriggerOpen\n    queueMicrotask(() => {\n      this.panel.focusPanel()\n      if (keyboard) this.panel.focusFirst()\n    })\n  }\n\n  private async hide(): Promise<void> {\n    this.panel.openSub?.setOpen(false)\n    this.state.set('closed')\n    const panel = this.panel.el\n    const hadFocus = !!panel?.contains(document.activeElement)\n    this.cleanups.splice(0).forEach((fn) => fn())\n    // Radix: focus goes back to the trigger only when the whole bar closed (not a switch) from inside.\n    const returnToTrigger = !this.menu.bar.currentValue && !this.interactedOutside\n    this.interactedOutside = false\n    const event = new Event('closeAutoFocus', { cancelable: true })\n    this.closeAutoFocus.emit(event)\n    if (returnToTrigger && hadFocus && !event.defaultPrevented) this.menu.trigger?.focus({ preventScroll: true })\n    await afterExitAnimation(panel)\n    if (this.menu.isOpen) return\n    this.portal.detach()\n    this.panel.el = undefined\n  }\n\n  /** ArrowLeft / ArrowRight move to the previous / next menu in the bar (not from sub-triggers / submenus). */\n  onKeydown(event: KeyboardEvent): void {\n    this.panel.keydown(event)\n    if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return\n    const target = event.target as HTMLElement\n    const prevKey = this.menu.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n    const isPrev = event.key === prevKey\n    if (!isPrev && target.hasAttribute('data-radix-menubar-subtrigger')) return\n    if (isPrev && target.closest('[data-radix-menubar-content]') !== this.panel.el) return\n    const bar = this.menu.bar\n    let values = bar.enabledTriggers().map((t) => t.menu.value)\n    if (isPrev) values.reverse()\n    const i = values.indexOf(this.menu.value)\n    values = bar.loop ? [...values.slice(i + 1), ...values.slice(0, i + 1)] : values.slice(i + 1)\n    const next = values[0]\n    if (next && next !== this.menu.value) bar.openMenu(next)\n  }\n\n  ngOnDestroy(): void {\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n}\n\n/** Radix Portal part: content already renders in a body portal, so this only groups. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-portal, [ui-menubar-portal]',\n  standalone: true,\n  host: { class: 'contents' },\n  template: `<ng-content />`,\n})\nexport class UiMenubarPortalComponent {}\n\n// ---------------------------------------------------------------------------------------\n// Items\n// ---------------------------------------------------------------------------------------\n\ntype ParentContent = UiMenubarContentComponent | UiMenubarSubContentComponent\n\n/** Shared item behaviour (select, keyboard, pointer highlight). Host metadata lives on each concrete item. */\n@Directive()\nabstract class MenubarItemBase {\n  protected readonly menu = inject(UiMenubarMenuComponent)\n  protected readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  protected readonly parent: ParentContent | null =\n    inject(UiMenubarSubContentComponent, { optional: true }) ?? inject(UiMenubarContentComponent, { optional: true })\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) disabled = false\n  /** Text used by typeahead when the item's text content is not what users type. */\n  @Input() textValue?: string\n  /** Radix `onSelect`: call `event.preventDefault()` to keep the menu open. */\n  @Output() select = new EventEmitter<Event>()\n  highlighted = false\n  private pointerWentDown = false\n\n  onClick(): void {\n    if (this.disabled) return\n    const event = new Event('select', { cancelable: true })\n    this.select.emit(event)\n    this.afterSelect()\n    if (!event.defaultPrevented) this.menu.setOpen(false)\n  }\n\n  /** Hook for checkbox / radio items to update their state before the menu closes. */\n  protected afterSelect(): void {}\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (event.key === ' ' && this.parent?.panel.search) return\n    if (event.key === 'Enter' || event.key === ' ') {\n      this.el.nativeElement.click()\n      event.preventDefault()\n    }\n  }\n\n  onPointerDown(): void {\n    this.pointerWentDown = true\n  }\n\n  /** Press on the trigger, release on an item (right-drag-release) selects it, like Radix. */\n  onPointerUp(): void {\n    if (!this.pointerWentDown) this.el.nativeElement.click()\n  }\n\n  onPointerMove(event: PointerEvent): void {\n    if (!isMouse(event) || !this.parent) return\n    if (this.disabled) return this.parent.panel.itemLeave(event)\n    if (this.parent.panel.isMovingToSub(event)) return\n    if (document.activeElement !== this.el.nativeElement) this.el.nativeElement.focus({ preventScroll: true })\n  }\n\n  onPointerLeave(event: PointerEvent): void {\n    if (isMouse(event)) this.parent?.panel.itemLeave(event)\n  }\n\n  onFocus(): void {\n    this.highlighted = true\n    this.parent?.panel.focusMoved(this.el.nativeElement)\n  }\n\n  onBlur(): void {\n    this.highlighted = false\n  }\n}\n\nconst ITEM_HOST = {\n  '[attr.data-uipkge]': '\"\"',\n  '[attr.tabindex]': '\"-1\"',\n  '[attr.data-orientation]': '\"vertical\"',\n  '[attr.data-highlighted]': 'highlighted ? \"\" : null',\n  '[attr.data-disabled]': 'disabled ? \"\" : null',\n  '[attr.aria-disabled]': 'disabled || null',\n  '[attr.data-text-value]': 'textValue ?? null',\n  '[class]': 'hostClass',\n  '(click)': 'onClick()',\n  '(keydown)': 'onKeydown($event)',\n  '(pointerdown)': 'onPointerDown()',\n  '(pointerup)': 'onPointerUp()',\n  '(pointermove)': 'onPointerMove($event)',\n  '(pointerleave)': 'onPointerLeave($event)',\n  '(focus)': 'onFocus()',\n  '(blur)': 'onBlur()',\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-item, [ui-menubar-item]',\n  standalone: true,\n  host: {\n    ...ITEM_HOST,\n    '[attr.data-slot]': '\"menubar-item\"',\n    '[attr.role]': '\"menuitem\"',\n    '[attr.data-variant]': 'variant',\n    '[attr.data-inset]': 'inset ? \"\" : null',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarItemComponent extends MenubarItemBase {\n  @Input({ transform: booleanAttribute }) inset = false\n  @Input() variant: MenubarItemVariant = 'default'\n\n  get hostClass(): string {\n    return cn(\n      \"focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:[&>svg,&>lucide-icon>svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n      this.className,\n    )\n  }\n}\n\nconst CHECKABLE_ITEM_CLASS =\n  \"focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-checkbox-item, [ui-menubar-checkbox-item]',\n  standalone: true,\n  host: {\n    ...ITEM_HOST,\n    '[attr.data-slot]': '\"menubar-checkbox-item\"',\n    '[attr.role]': '\"menuitemcheckbox\"',\n    '[attr.aria-checked]': 'checked === \"indeterminate\" ? \"mixed\" : checked',\n    '[attr.data-state]': 'checkedState',\n  },\n  template: `\n    <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n      @if (checked) {\n        <span [attr.data-state]=\"checkedState\"\n          ><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-check size-4\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M20 6 9 17l-5-5\" /></svg\n        ></span>\n      }\n    </span>\n    <ng-content />\n  `,\n})\nexport class UiMenubarCheckboxItemComponent extends MenubarItemBase {\n  private readonly _checkedProp = signal<MenubarCheckedState | undefined>(undefined)\n  private readonly _internal = signal<MenubarCheckedState | null>(null)\n  /** Controlled checked state (pair with `checkedChange` for `[(checked)]`). */\n  @Input()\n  set checked(v: MenubarCheckedState | undefined) {\n    this._checkedProp.set(v)\n  }\n  get checked(): MenubarCheckedState {\n    return this._checkedProp() ?? this._internal() ?? this.defaultChecked\n  }\n  /** Uncontrolled initial state (Radix `defaultChecked`). */\n  @Input() defaultChecked: MenubarCheckedState = false\n  @Output() checkedChange = new EventEmitter<boolean>()\n\n  get checkedState(): 'checked' | 'unchecked' | 'indeterminate' {\n    return this.checked === 'indeterminate' ? 'indeterminate' : this.checked ? 'checked' : 'unchecked'\n  }\n\n  get hostClass(): string {\n    return cn(CHECKABLE_ITEM_CLASS, this.className)\n  }\n\n  protected override afterSelect(): void {\n    const next = this.checked === 'indeterminate' ? true : !this.checked\n    if (this._checkedProp() === undefined) this._internal.set(next)\n    this.checkedChange.emit(next)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-radio-group, [ui-menubar-radio-group]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-radio-group\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n    class: 'block',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarRadioGroupComponent {\n  private readonly _valueProp = signal<string | undefined>(undefined)\n  private readonly _internal = signal<string | null>(null)\n  /** Controlled value (pair with `valueChange` for `[(value)]`). */\n  @Input()\n  set value(v: string | undefined) {\n    this._valueProp.set(v ?? undefined)\n  }\n  get value(): string | undefined {\n    return this._valueProp() ?? this._internal() ?? this.defaultValue\n  }\n  /** Uncontrolled initial value (Radix `defaultValue`). */\n  @Input() defaultValue?: string\n  @Output() valueChange = new EventEmitter<string>()\n\n  setValue(value: string): void {\n    if (this._valueProp() === undefined) this._internal.set(value)\n    this.valueChange.emit(value)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-radio-item, [ui-menubar-radio-item]',\n  standalone: true,\n  host: {\n    ...ITEM_HOST,\n    '[attr.data-slot]': '\"menubar-radio-item\"',\n    '[attr.role]': '\"menuitemradio\"',\n    '[attr.aria-checked]': 'isChecked',\n    '[attr.data-state]': 'isChecked ? \"checked\" : \"unchecked\"',\n  },\n  template: `\n    <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n      @if (isChecked) {\n        <span data-state=\"checked\"\n          ><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-circle size-2 fill-current\"\n            aria-hidden=\"true\"\n          >\n            <circle cx=\"12\" cy=\"12\" r=\"10\" /></svg\n        ></span>\n      }\n    </span>\n    <ng-content />\n  `,\n})\nexport class UiMenubarRadioItemComponent extends MenubarItemBase {\n  private readonly group = inject(UiMenubarRadioGroupComponent, { optional: true })\n  @Input() value = ''\n\n  get isChecked(): boolean {\n    return !!this.group && this.group.value === this.value\n  }\n\n  get hostClass(): string {\n    return cn(CHECKABLE_ITEM_CLASS, this.className)\n  }\n\n  protected override afterSelect(): void {\n    this.group?.setValue(this.value)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-label, [ui-menubar-label]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-label\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-inset]': 'inset ? \"\" : null',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarLabelComponent {\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) inset = false\n\n  get hostClass(): string {\n    return cn('block px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-separator, [ui-menubar-separator]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-separator\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"separator\"',\n    '[attr.aria-orientation]': '\"horizontal\"',\n    '[class]': 'hostClass',\n  },\n  template: ``,\n})\nexport class UiMenubarSeparatorComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('block bg-border -mx-1 my-1 h-px', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-shortcut, [ui-menubar-shortcut]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-shortcut\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarShortcutComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('text-muted-foreground ml-auto text-xs tracking-widest', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-group, [ui-menubar-group]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-group\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n    class: 'block',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarGroupComponent {}\n\n// ---------------------------------------------------------------------------------------\n// Submenus\n// ---------------------------------------------------------------------------------------\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-sub, [ui-menubar-sub]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"menubar-sub\"',\n    '[attr.data-uipkge]': '\"\"',\n    class: 'contents',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMenubarSubComponent implements OnChanges {\n  private readonly parent: ParentContent | null =\n    inject(UiMenubarSubContentComponent, { optional: true }) ?? inject(UiMenubarContentComponent, { optional: true })\n  @Input() open?: boolean\n  @Input() defaultOpen = false\n  @Output() openChange = new EventEmitter<boolean>()\n\n  readonly contentId = uniqueId('menubar-sub-content')\n  private readonly _open = signal<boolean | null>(null)\n  trigger?: HTMLElement\n  content?: UiMenubarSubContentComponent\n\n  get isOpen(): boolean {\n    if (this.open !== undefined) return this.open\n    return this._open() ?? this.defaultOpen\n  }\n\n  ngOnChanges(): void {\n    this.content?.sync()\n  }\n\n  setOpen(value: boolean): void {\n    if (value === this.isOpen) return\n    this._open.set(value)\n    this.openChange.emit(value)\n    if (this.parent) {\n      if (value) this.parent.panel.openSub = this\n      else if (this.parent.panel.openSub === this) this.parent.panel.openSub = undefined\n    }\n    this.content?.sync()\n  }\n\n  containsTarget(target: Node): boolean {\n    return !!this.content?.panelEl?.contains(target) || !!this.content?.panel.openSub?.containsTarget(target)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-sub-trigger, [ui-menubar-sub-trigger]',\n  standalone: true,\n  host: {\n    ...ITEM_HOST,\n    '[attr.data-slot]': '\"menubar-sub-trigger\"',\n    '[attr.data-radix-menubar-subtrigger]': '\"\"',\n    '[attr.role]': '\"menuitem\"',\n    '[attr.aria-haspopup]': '\"menu\"',\n    '[attr.aria-expanded]': 'sub.isOpen',\n    '[attr.aria-controls]': 'sub.isOpen ? sub.contentId : null',\n    '[attr.data-state]': 'sub.isOpen ? \"open\" : \"closed\"',\n    '[attr.data-inset]': 'inset ? \"\" : null',\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-right ml-auto size-4\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m9 18 6-6-6-6\" />\n    </svg>`,\n})\nexport class UiMenubarSubTriggerComponent extends MenubarItemBase implements OnDestroy {\n  readonly sub = inject(UiMenubarSubComponent)\n  @Input({ transform: booleanAttribute }) inset = false\n  private openTimer: ReturnType<typeof setTimeout> | undefined\n\n  constructor() {\n    super()\n    this.sub.trigger = this.el.nativeElement\n  }\n\n  get hostClass(): string {\n    return cn(\n      'focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:ring-2 focus-visible:ring-inset data-[inset]:pl-8',\n      this.className,\n    )\n  }\n\n  private openSub(): void {\n    this.sub.setOpen(true)\n  }\n\n  /** Click focuses and opens; it never selects / closes the menu. */\n  override onClick(): void {\n    if (this.disabled) return\n    this.el.nativeElement.focus({ preventScroll: true })\n    if (!this.sub.isOpen) this.openSub()\n  }\n\n  override onPointerUp(): void {}\n\n  /** Hover opens after 100ms (Radix), unless the pointer is heading into another open submenu. */\n  override onPointerMove(event: PointerEvent): void {\n    super.onPointerMove(event)\n    if (!isMouse(event) || event.defaultPrevented || this.disabled || this.sub.isOpen || this.openTimer) return\n    if (this.parent?.panel.isMovingToSub(event)) return\n    this.parent?.panel.setGrace(null)\n    this.openTimer = setTimeout(() => {\n      this.openTimer = undefined\n      this.openSub()\n    }, 100)\n  }\n\n  /** Leaving toward the open submenu keeps it open through a triangle grace area. */\n  override onPointerLeave(event: PointerEvent): void {\n    if (!isMouse(event)) return\n    clearTimeout(this.openTimer)\n    this.openTimer = undefined\n    const content = this.sub.content?.panelEl\n    if (!this.sub.isOpen || !content) return this.parent?.panel.itemLeave(event)\n    const rect = content.getBoundingClientRect()\n    const right = (content.getAttribute('data-side') ?? 'right') === 'right'\n    const near = right ? rect.left : rect.right\n    const far = right ? rect.right : rect.left\n    this.parent?.panel.setGrace({\n      side: right ? 'right' : 'left',\n      area: [\n        { x: event.clientX + (right ? -5 : 5), y: event.clientY },\n        { x: near, y: rect.top },\n        { x: far, y: rect.top },\n        { x: far, y: rect.bottom },\n        { x: near, y: rect.bottom },\n      ],\n    })\n  }\n\n  override onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (event.key === ' ' && this.parent?.panel.search) return\n    const openKey = this.menu.dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n    if (event.key === 'Enter' || event.key === ' ' || event.key === openKey) {\n      event.preventDefault()\n      event.stopPropagation()\n      this.openSub()\n      this.sub.content?.focusFirstWhenOpen()\n    }\n  }\n\n  ngOnDestroy(): void {\n    clearTimeout(this.openTimer)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-menubar-sub-content, [ui-menubar-sub-content]',\n  standalone: true,\n  host: { class: 'hidden' },\n  template: `\n    <ng-template #panelTpl>\n      <div\n        [id]=\"sub.contentId\"\n        role=\"menu\"\n        aria-orientation=\"vertical\"\n        data-orientation=\"vertical\"\n        tabindex=\"-1\"\n        data-radix-menubar-content=\"\"\n        data-slot=\"menubar-sub-content\"\n        data-uipkge=\"\"\n        [attr.dir]=\"menu.dir\"\n        [attr.data-state]=\"state()\"\n        [class]=\"panelClass\"\n        (keydown)=\"onKeydown($event)\"\n        (pointermove)=\"panel.pointerMove($event)\"\n        (focus)=\"panel.focusMoved($event.target)\"\n      >\n        <ng-content />\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiMenubarSubContentComponent implements OnDestroy {\n  readonly sub = inject(UiMenubarSubComponent)\n  readonly menu = inject(UiMenubarMenuComponent)\n  private readonly parent: ParentContent | null =\n    inject(UiMenubarSubContentComponent, { optional: true, skipSelf: true }) ??\n    inject(UiMenubarContentComponent, { optional: true })\n  private readonly portal = new BodyPortal(inject(ViewContainerRef))\n\n  @Input('class') className?: string\n  @Input() sideOffset = 0\n  @Input() alignOffset = 0\n  @Input({ transform: booleanAttribute }) avoidCollisions = true\n  @Input() collisionPadding = 0\n  @Input({ transform: booleanAttribute }) loop = false\n  @ViewChild('panelTpl', { static: true }) panelTpl!: TemplateRef<unknown>\n  readonly panel = new MenuPanel(() => this.loop)\n  readonly state = signal<'open' | 'closed'>('closed')\n  private cleanups: (() => void)[] = []\n\n  constructor() {\n    this.sub.content = this\n  }\n\n  get panelEl(): HTMLElement | undefined {\n    return this.panel.el\n  }\n\n  get panelClass(): string {\n    return cn(\n      'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',\n      this.className,\n    )\n  }\n\n  sync(): void {\n    if (this.sub.isOpen && (!this.portal.attached || this.state() === 'closed')) {\n      if (this.portal.attached) this.portal.detach()\n      this.show()\n    } else if (!this.sub.isOpen && this.portal.attached && this.state() === 'open') void this.hide()\n  }\n\n  /** Keyboard opens (ArrowRight / Enter / Space on the sub-trigger) land on the first item. */\n  focusFirstWhenOpen(): void {\n    queueMicrotask(() => this.panel.focusFirst())\n  }\n\n  private show(): void {\n    const trigger = this.sub.trigger\n    if (!trigger) return\n    this.state.set('open')\n    const panel = this.portal.attach(this.panelTpl).firstElementChild as HTMLElement\n    this.panel.el = panel\n    this.cleanups.push(\n      placeAndTrack(\n        trigger,\n        panel,\n        () => ({\n          side: this.menu.dir === 'rtl' ? 'left' : 'right',\n          align: 'start',\n          sideOffset: this.sideOffset,\n          alignOffset: this.alignOffset,\n          collisionPadding: this.collisionPadding,\n          avoidCollisions: this.avoidCollisions,\n        }),\n        'menubar',\n      ),\n      pushDismissableLayer({\n        contains: (t) => panel.contains(t) || trigger.contains(t) || !!this.panel.openSub?.containsTarget(t),\n        // Radix: Escape inside a submenu closes the whole menu.\n        onEscape: () => this.menu.setOpen(false),\n        // Clicking back inside the parent menu closes only this submenu; anywhere else closes the whole menu.\n        onPointerDownOutside: (e) => {\n          if (this.parent?.panelEl?.contains(e.target as Node)) this.sub.setOpen(false)\n          else this.menu.setOpen(false)\n        },\n      }),\n      () => this.panel.destroy(),\n    )\n  }\n\n  private async hide(): Promise<void> {\n    this.panel.openSub?.setOpen(false)\n    this.state.set('closed')\n    const panel = this.panel.el\n    this.cleanups.splice(0).forEach((fn) => fn())\n    await afterExitAnimation(panel)\n    if (this.sub.isOpen) return\n    this.portal.detach()\n    this.panel.el = undefined\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    const closeKey = this.menu.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n    if (event.key === closeKey && this.panel.el?.contains(event.target as Node)) {\n      event.preventDefault()\n      this.sub.setOpen(false)\n      this.sub.trigger?.focus({ preventScroll: true })\n      return\n    }\n    this.panel.keydown(event)\n  }\n\n  ngOnDestroy(): void {\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/menubar/menubar.component.ts"
    },
    {
      "path": "packages/registry-angular/components/menubar/index.ts",
      "content": "export {\n  UiMenubarComponent,\n  UiMenubarMenuComponent,\n  UiMenubarTriggerComponent,\n  UiMenubarContentComponent,\n  UiMenubarPortalComponent,\n  UiMenubarItemComponent,\n  UiMenubarCheckboxItemComponent,\n  UiMenubarRadioGroupComponent,\n  UiMenubarRadioItemComponent,\n  UiMenubarLabelComponent,\n  UiMenubarSeparatorComponent,\n  UiMenubarShortcutComponent,\n  UiMenubarGroupComponent,\n  UiMenubarSubComponent,\n  UiMenubarSubTriggerComponent,\n  UiMenubarSubContentComponent,\n  MENUBAR_CONTENT_CLASS,\n  type MenubarItemVariant,\n  type MenubarCheckedState,\n  type MenubarDir,\n  type MenubarSide,\n  type MenubarAlign,\n} from './menubar.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/menubar/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Top-level menu bar — File / Edit / View — for desktop-style apps. Same item family as Dropdown Menu (items, checkbox/radio items, labels, separators, shortcuts, submenus). Radix Menubar behaviour: roving focus across triggers, ArrowLeft/Right move between open menus, hovering another trigger switches menus; portalled + positioned with flip/shift.",
  "categories": [
    "navigation"
  ]
}