{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dropdown-menu",
  "title": "Dropdown Menu",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/dropdown-menu/dropdown-menu.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\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  disableOutsidePointerEvents,\n  lockScroll,\n  pushDismissableLayer,\n  uniqueId,\n  type PopperAlign,\n} from '@/ui/popper/popper'\nimport { dropdownMenuContentVariants } from './dropdown-menu-content.variants'\n\nexport type DropdownMenuSide = 'top' | 'right' | 'bottom' | 'left'\nexport type DropdownMenuAlign = PopperAlign\nexport type DropdownMenuItemVariant = 'default' | 'destructive'\n\n/**\n * Angular port of UIPKGE Dropdown Menu, behaving like the Radix (React) / reka-ui\n * (Vue) menu: the trigger is a directive (the `asChild` equivalent -- put it on any\n * button), content renders in a body portal positioned against the trigger with\n * flip + shift, arrow keys / Home / End / typeahead move focus, Escape and outside\n * clicks dismiss, selecting an item closes the menu, and submenus open to the side.\n * Tailwind class strings are identical to the Vue / React sources.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu, [ui-dropdown-menu]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n    // Radix Root renders no element: keep the wrapper out of layout.\n    class: 'contents',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuComponent implements OnChanges {\n  @Input() open?: boolean\n  @Input() defaultOpen = false\n  @Input({ transform: booleanAttribute }) modal = true\n  @Output() openChange = new EventEmitter<boolean>()\n\n  readonly contentId = uniqueId('dropdown-menu-content')\n  private readonly _open = signal<boolean | null>(null)\n  trigger?: HTMLElement\n  content?: UiDropdownMenuContentComponent\n  /** How the last open happened: keyboard opens focus the first item, pointer opens focus the menu. */\n  openedWith: 'keyboard' | 'pointer' = 'pointer'\n  focusLastOnOpen = false\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    this.content?.sync()\n  }\n\n  toggle(): void {\n    this.setOpen(!this.isOpen)\n  }\n}\n\n@Directive({\n  selector: 'ui-dropdown-menu-trigger, [ui-dropdown-menu-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'dataSlot',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.aria-haspopup]': '\"menu\"',\n    '[attr.aria-expanded]': 'menu.isOpen',\n    '[attr.aria-controls]': 'menu.isOpen ? menu.contentId : null',\n    '[attr.data-state]': 'menu.isOpen ? \"open\" : \"closed\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '(click)': 'onClick()',\n    '(keydown)': 'onKeydown($event)',\n  },\n})\nexport class UiDropdownMenuTriggerComponent {\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') ?? 'dropdown-menu-trigger'\n  readonly menu = inject(UiDropdownMenuComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  @Input({ transform: booleanAttribute }) disabled = false\n\n  constructor() {\n    this.menu.trigger = this.el.nativeElement\n  }\n\n  onClick(): void {\n    if (this.disabled) return\n    this.menu.openedWith = 'pointer'\n    this.menu.toggle()\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key)) {\n      event.preventDefault()\n      this.menu.openedWith = 'keyboard'\n      this.menu.focusLastOnOpen = event.key === 'ArrowUp'\n      this.menu.setOpen(true)\n    }\n  }\n}\n\n/** Roving focus + typeahead shared by the root content and submenu content. */\nfunction menuItems(panel: HTMLElement): HTMLElement[] {\n  return [...panel.querySelectorAll<HTMLElement>('[role^=\"menuitem\"]')].filter(\n    (el) => !el.hasAttribute('data-disabled'),\n  )\n}\n\nfunction handleMenuKeydown(event: KeyboardEvent, panel: HTMLElement, loop = false): void {\n  const items = menuItems(panel)\n  const current = items.indexOf(document.activeElement as HTMLElement)\n  // Radix: arrows stop at the ends unless `loop` is set.\n  const focusAt = (i: number) =>\n    items[loop ? (i + items.length) % items.length : Math.max(0, Math.min(items.length - 1, i))]?.focus()\n  if (event.key === 'ArrowDown') focusAt(current + 1)\n  else if (event.key === 'ArrowUp') focusAt(current < 0 ? items.length - 1 : current - 1)\n  else if (event.key === 'Home' || event.key === 'PageUp') focusAt(0)\n  else if (event.key === 'End' || event.key === 'PageDown') focusAt(items.length - 1)\n  else if (event.key === 'Tab') {\n    /* Radix keeps focus inside an open menu. */\n  } else if (event.key.length === 1 && /\\S/.test(event.key) && !event.metaKey && !event.ctrlKey) {\n    const q = event.key.toLowerCase()\n    const order = [...items.slice(current + 1), ...items.slice(0, current + 1)]\n    order.find((el) => el.textContent?.trim().toLowerCase().startsWith(q))?.focus()\n    return\n  } else return\n  event.preventDefault()\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-content, [ui-dropdown-menu-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 #panel>\n      <div\n        #panelEl\n        [id]=\"menu.contentId\"\n        role=\"menu\"\n        aria-orientation=\"vertical\"\n        tabindex=\"-1\"\n        data-slot=\"dropdown-menu-content\"\n        data-uipkge=\"\"\n        [attr.data-state]=\"state()\"\n        [class]=\"panelClass\"\n        (keydown)=\"onKeydown($event)\"\n        (pointerleave)=\"onPointerLeave()\"\n      >\n        <ng-content />\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiDropdownMenuContentComponent implements OnChanges, OnDestroy {\n  readonly menu = inject(UiDropdownMenuComponent)\n  private readonly portal = new BodyPortal(inject(ViewContainerRef))\n\n  @Input('class') className?: string\n  @Input() side: DropdownMenuSide = 'bottom'\n  @Input() align: DropdownMenuAlign = 'center'\n  @Input() sideOffset = 4\n  @Input() alignOffset = 0\n  /** Radix `loop`: arrow keys wrap from last to first item. */\n  @Input({ transform: booleanAttribute }) loop = false\n  @Input({ transform: booleanAttribute }) avoidCollisions = true\n  @Input() collisionPadding = 0\n  @Output() closeAutoFocus = new EventEmitter<Event>()\n\n  @ViewChild('panel', { static: true }) panelTpl!: TemplateRef<unknown>\n  panelEl?: HTMLElement\n  readonly state = signal<'open' | 'closed'>('closed')\n  openSub?: UiDropdownMenuSubComponent\n  private cleanups: (() => void)[] = []\n\n  constructor() {\n    this.menu.content = this\n  }\n\n  get panelClass(): string {\n    return cn(dropdownMenuContentVariants({ side: this.side }), this.className)\n  }\n\n  ngOnChanges(): void {\n    if (this.portal.attached) this.sync()\n  }\n\n  /** Mirrors root open state into the portal (called by the root on every change). */\n  sync(): void {\n    if (this.menu.isOpen && (!this.portal.attached || this.state() === 'closed')) {\n      // Reopened mid exit-animation: drop the closing panel and start fresh.\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.state.set('open')\n    const host = this.portal.attach(this.panelTpl)\n    const panel = host.firstElementChild as HTMLElement\n    this.panelEl = panel\n    this.cleanups.push(\n      autoPlace(trigger, panel, () => this.placeOptions(), 'dropdown-menu'),\n      pushDismissableLayer({\n        contains: (t) => panel.contains(t) || trigger.contains(t) || !!this.openSub?.containsTarget(t),\n        onEscape: () => this.menu.setOpen(false),\n        onPointerDownOutside: () => this.menu.setOpen(false),\n      }),\n    )\n    if (this.menu.modal) this.cleanups.push(lockScroll(), disableOutsidePointerEvents(panel))\n    queueMicrotask(() => {\n      const items = menuItems(panel)\n      if (this.menu.openedWith === 'keyboard' && items.length) {\n        ;(this.menu.focusLastOnOpen ? items[items.length - 1] : items[0])!.focus()\n      } else panel.focus({ preventScroll: true })\n    })\n  }\n\n  private async hide(): Promise<void> {\n    this.openSub?.setOpen(false)\n    this.state.set('closed')\n    const panel = this.panelEl\n    const hadFocus = !!panel?.contains(document.activeElement)\n    this.cleanups.splice(0).forEach((fn) => fn())\n    await afterExitAnimation(panel)\n    if (this.menu.isOpen) return\n    this.portal.detach()\n    this.panelEl = undefined\n    const event = new Event('closeAutoFocus', { cancelable: true })\n    this.closeAutoFocus.emit(event)\n    if (hadFocus && !event.defaultPrevented) this.menu.trigger?.focus({ preventScroll: true })\n  }\n\n  private placeOptions() {\n    return {\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  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.panelEl) handleMenuKeydown(event, this.panelEl, this.loop)\n  }\n\n  onPointerLeave(): void {\n    if (!this.openSub?.isOpen) this.panelEl?.focus({ preventScroll: true })\n  }\n\n  ngOnDestroy(): void {\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n}\n\n/** Shared item behaviour (select, keyboard, pointer highlight). Host metadata lives on each concrete item. */\n@Directive()\nabstract class DropdownMenuItemBase {\n  protected readonly menu = inject(UiDropdownMenuComponent)\n  protected readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  private readonly parentContent =\n    inject(UiDropdownMenuSubContentComponent, { optional: true }) ??\n    inject(UiDropdownMenuContentComponent, { optional: true })\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) inset = false\n  @Input() variant: DropdownMenuItemVariant = 'default'\n  @Input({ transform: booleanAttribute }) disabled = false\n  /** Radix `onSelect`: call `event.preventDefault()` to keep the menu open. */\n  @Output() select = new EventEmitter<Event>()\n\n  onSelect(_event?: Event): void {\n    if (this.disabled) return\n    const event = new Event('select', { cancelable: true })\n    this.beforeSelect()\n    this.select.emit(event)\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 beforeSelect(): void {}\n\n  onKeydown(event: KeyboardEvent): void {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      this.onSelect()\n    }\n  }\n\n  onPointerMove(): void {\n    if (this.disabled) return\n    this.parentContent?.openSub?.setOpen(false)\n    if (document.activeElement !== this.el.nativeElement) this.el.nativeElement.focus({ preventScroll: true })\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-item, [ui-dropdown-menu-item]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-item\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"menuitem\"',\n    '[attr.data-variant]': 'variant',\n    '[attr.data-inset]': 'inset ? \"\" : null',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.aria-disabled]': 'disabled || null',\n    '[attr.tabindex]': '\"-1\"',\n    '[class]': 'hostClass',\n    '(click)': 'onSelect($event)',\n    '(keydown)': 'onKeydown($event)',\n    '(pointermove)': 'onPointerMove()',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuItemComponent extends DropdownMenuItemBase {\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/20 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\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-checkbox-item, [ui-dropdown-menu-checkbox-item]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-checkbox-item\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"menuitemcheckbox\"',\n    '[attr.aria-checked]': 'checked',\n    '[attr.data-state]': 'checked ? \"checked\" : \"unchecked\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.tabindex]': '\"-1\"',\n    '[class]': 'hostClass',\n    '(click)': 'onSelect($event)',\n    '(keydown)': 'onKeydown($event)',\n    '(pointermove)': 'onPointerMove()',\n  },\n  template: `\n    <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n      @if (checked) {\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\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=\"size-4\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M20 6 9 17l-5-5\" />\n        </svg>\n      }\n    </span>\n    <ng-content />\n  `,\n})\nexport class UiDropdownMenuCheckboxItemComponent extends DropdownMenuItemBase {\n  @Input({ transform: booleanAttribute }) checked = false\n  @Output() checkedChange = new EventEmitter<boolean>()\n\n  get hostClass(): string {\n    return cn(\n      \"focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring relative flex cursor-default items-center gap-2 rounded-sm 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      this.className,\n    )\n  }\n\n  protected override beforeSelect(): void {\n    this.checked = !this.checked\n    this.checkedChange.emit(this.checked)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-radio-group, [ui-dropdown-menu-radio-group]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-radio-group\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuRadioGroupComponent {\n  @Input() value?: string\n  @Output() valueChange = new EventEmitter<string>()\n\n  setValue(value: string): void {\n    this.value = value\n    this.valueChange.emit(value)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-radio-item, [ui-dropdown-menu-radio-item]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-radio-item\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"menuitemradio\"',\n    '[attr.aria-checked]': 'isChecked',\n    '[attr.data-state]': 'isChecked ? \"checked\" : \"unchecked\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.tabindex]': '\"-1\"',\n    '[class]': 'hostClass',\n    '(click)': 'onSelect($event)',\n    '(keydown)': 'onKeydown($event)',\n    '(pointermove)': 'onPointerMove()',\n  },\n  template: `\n    <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n      @if (isChecked) {\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 24 24\"\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          class=\"size-2 fill-current\"\n          aria-hidden=\"true\"\n        >\n          <circle cx=\"12\" cy=\"12\" r=\"10\" />\n        </svg>\n      }\n    </span>\n    <ng-content />\n  `,\n})\nexport class UiDropdownMenuRadioItemComponent extends DropdownMenuItemBase {\n  private readonly group = inject(UiDropdownMenuRadioGroupComponent, { optional: true })\n  @Input() value?: string\n  @Input({ transform: booleanAttribute }) checked = false\n\n  get isChecked(): boolean {\n    return this.group && this.value !== undefined ? this.group.value === this.value : this.checked\n  }\n\n  get hostClass(): string {\n    return cn(\n      \"focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring relative flex cursor-default items-center gap-2 rounded-sm 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      this.className,\n    )\n  }\n\n  protected override beforeSelect(): void {\n    if (this.group && this.value !== undefined) this.group.setValue(this.value)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-label, [ui-dropdown-menu-label]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-label\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-inset]': 'inset ? \"\" : null',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuLabelComponent {\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-dropdown-menu-separator, [ui-dropdown-menu-separator]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-separator\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"separator\"',\n    '[class]': 'hostClass',\n  },\n  template: ``,\n})\nexport class UiDropdownMenuSeparatorComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('bg-border -mx-1 my-1 block h-px', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-shortcut, [ui-dropdown-menu-shortcut]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-shortcut\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuShortcutComponent {\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-dropdown-menu-group, [ui-dropdown-menu-group]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-group\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n    class: 'block',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuGroupComponent {}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-sub, [ui-dropdown-menu-sub]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-sub\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n    class: 'contents',\n  },\n  template: `<ng-content />`,\n})\nexport class UiDropdownMenuSubComponent implements OnChanges {\n  private readonly parentContent =\n    inject(UiDropdownMenuSubContentComponent, { optional: true, skipSelf: true }) ??\n    inject(UiDropdownMenuContentComponent, { optional: true })\n  @Input() open?: boolean\n  @Input() defaultOpen = false\n  @Output() openChange = new EventEmitter<boolean>()\n\n  readonly contentId = uniqueId('dropdown-menu-sub-content')\n  private readonly _open = signal<boolean | null>(null)\n  trigger?: HTMLElement\n  content?: UiDropdownMenuSubContentComponent\n  openedWith: 'keyboard' | 'pointer' = 'pointer'\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.parentContent) this.parentContent.openSub = value ? this : undefined\n    this.content?.sync()\n  }\n\n  containsTarget(target: Node): boolean {\n    return !!this.content?.panelEl?.contains(target) || !!this.content?.openSub?.containsTarget(target)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-sub-trigger, [ui-dropdown-menu-sub-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dropdown-menu-sub-trigger\"',\n    '[attr.data-uipkge]': '\"\"',\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    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.aria-disabled]': 'disabled || null',\n    '[attr.tabindex]': '\"-1\"',\n    '[class]': 'hostClass',\n    '(pointermove)': 'onPointerMove()',\n    '(click)': 'openSub(\"pointer\")',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `<ng-content /><svg\n      xmlns=\"http://www.w3.org/2000/svg\"\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=\"ml-auto size-4\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m9 18 6-6-6-6\" />\n    </svg>`,\n})\nexport class UiDropdownMenuSubTriggerComponent {\n  readonly sub = inject(UiDropdownMenuSubComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) inset = false\n  @Input({ transform: booleanAttribute }) disabled = false\n\n  constructor() {\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 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-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n      this.className,\n    )\n  }\n\n  openSub(how: 'keyboard' | 'pointer'): void {\n    if (this.disabled) return\n    this.sub.openedWith = how\n    this.sub.setOpen(true)\n  }\n\n  onPointerMove(): void {\n    if (this.disabled) return\n    if (document.activeElement !== this.el.nativeElement) this.el.nativeElement.focus({ preventScroll: true })\n    this.openSub('pointer')\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    if (event.key === 'ArrowRight' || event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      event.stopPropagation()\n      this.openSub('keyboard')\n    }\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dropdown-menu-sub-content, [ui-dropdown-menu-sub-content]',\n  standalone: true,\n  host: { class: 'hidden' },\n  template: `\n    <ng-template #panel>\n      <div\n        [id]=\"sub.contentId\"\n        role=\"menu\"\n        aria-orientation=\"vertical\"\n        tabindex=\"-1\"\n        data-slot=\"dropdown-menu-sub-content\"\n        data-uipkge=\"\"\n        [attr.data-state]=\"state()\"\n        [class]=\"panelClass\"\n        (keydown)=\"onKeydown($event)\"\n      >\n        <ng-content />\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiDropdownMenuSubContentComponent implements OnDestroy {\n  readonly sub = inject(UiDropdownMenuSubComponent)\n  private readonly menu = inject(UiDropdownMenuComponent)\n  private readonly rootContent = inject(UiDropdownMenuContentComponent, { 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 }) loop = false\n  @ViewChild('panel', { static: true }) panelTpl!: TemplateRef<unknown>\n  panelEl?: HTMLElement\n  openSub?: UiDropdownMenuSubComponent\n  readonly state = signal<'open' | 'closed'>('closed')\n  private cleanups: (() => void)[] = []\n\n  constructor() {\n    this.sub.content = this\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-(--reka-dropdown-menu-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      // Reopened mid exit-animation: drop the closing panel and start fresh.\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  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.panelEl = panel\n    this.cleanups.push(\n      autoPlace(\n        trigger,\n        panel,\n        () => ({\n          side: 'right',\n          align: 'start',\n          sideOffset: this.sideOffset,\n          alignOffset: this.alignOffset,\n          collisionPadding: 0,\n          avoidCollisions: true,\n        }),\n        'dropdown-menu',\n      ),\n      pushDismissableLayer({\n        contains: (t) => panel.contains(t) || trigger.contains(t) || !!this.openSub?.containsTarget(t),\n        // Radix: Escape inside a submenu closes the whole menu (ArrowLeft closes just the sub).\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          const inParent = this.rootContent?.panelEl?.contains(e.target as Node)\n          if (inParent) this.sub.setOpen(false)\n          else this.menu.setOpen(false)\n        },\n      }),\n    )\n    if (this.menu.modal) this.cleanups.push(disableOutsidePointerEvents(panel))\n    if (this.sub.openedWith === 'keyboard') queueMicrotask(() => menuItems(panel)[0]?.focus())\n  }\n\n  private async hide(): Promise<void> {\n    this.openSub?.setOpen(false)\n    this.state.set('closed')\n    const panel = this.panelEl\n    this.cleanups.splice(0).forEach((fn) => fn())\n    await afterExitAnimation(panel)\n    if (this.sub.isOpen) return\n    this.portal.detach()\n    this.panelEl = undefined\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (!this.panelEl) return\n    if (event.key === 'ArrowLeft') {\n      event.preventDefault()\n      event.stopPropagation()\n      this.sub.setOpen(false)\n      this.sub.trigger?.focus({ preventScroll: true })\n      return\n    }\n    event.stopPropagation()\n    handleMenuKeydown(event, this.panelEl, this.loop)\n  }\n\n  ngOnDestroy(): void {\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n}\n\nexport { dropdownMenuContentVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/dropdown-menu/dropdown-menu.component.ts"
    },
    {
      "path": "packages/registry-angular/components/dropdown-menu/dropdown-menu-content.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { 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 */\nexport const dropdownMenuContentVariants = cva(\n  'z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md 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',\n  {\n    variants: {\n      side: {\n        bottom: 'data-[side=bottom]:slide-in-from-top-2',\n        left: 'data-[side=left]:slide-in-from-right-2',\n        right: 'data-[side=right]:slide-in-from-left-2',\n        top: 'data-[side=top]:slide-in-from-bottom-2',\n      },\n    },\n  },\n)\n\nexport type DropdownMenuContentVariants = VariantProps<typeof dropdownMenuContentVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/dropdown-menu/dropdown-menu-content.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/dropdown-menu/index.ts",
      "content": "export {\n  UiDropdownMenuComponent,\n  UiDropdownMenuTriggerComponent,\n  UiDropdownMenuContentComponent,\n  UiDropdownMenuItemComponent,\n  UiDropdownMenuCheckboxItemComponent,\n  UiDropdownMenuRadioGroupComponent,\n  UiDropdownMenuRadioItemComponent,\n  UiDropdownMenuLabelComponent,\n  UiDropdownMenuSeparatorComponent,\n  UiDropdownMenuShortcutComponent,\n  UiDropdownMenuGroupComponent,\n  UiDropdownMenuSubComponent,\n  UiDropdownMenuSubTriggerComponent,\n  UiDropdownMenuSubContentComponent,\n  dropdownMenuContentVariants,\n  type DropdownMenuSide,\n  type DropdownMenuAlign,\n  type DropdownMenuItemVariant,\n} from './dropdown-menu.component'\nexport { dropdownMenuContentVariants as dropdownMenuContentVariantsFromFile } from './dropdown-menu-content.variants'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/dropdown-menu/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Floating menu launched from a trigger button — for account switchers, row actions, editor menus, and any short list of commands. Labels, icons, separators, shortcuts, checkbox/radio items, and nested submenus. Portalled + positioned like Radix (flip, shift, keyboard nav, typeahead).",
  "categories": [
    "overlay"
  ]
}