{"name":"dropdown-menu","type":"registry:component","title":"Dropdown Menu","description":"Displays a menu to the user—such as a set of actions or functions—triggered by a button. Supports nested submenus, checkbox items, radio groups, keyboard navigation, and accessibility features.","categories":["ui","menu","dropdown","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","registryDependencies":["@lit/popover"],"dependencies":["lucide-static","@floating-ui/dom"],"files":[{"path":"registry/ui/dropdown-menu/dropdown-menu.ts","type":"registry:ui","content":"import { css, html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport {\n  customElement,\n  property,\n  query,\n  queryAssignedElements,\n  state,\n} from \"lit/decorators.js\";\nimport { unsafeSVG } from \"lit/directives/unsafe-svg.js\";\nimport { Check, ChevronRight, Circle } from \"lucide-static\";\nimport { TW } from \"@/registry/lib/tailwindMixin\";\nimport { cn } from \"@/registry/lib/utils\";\nimport \"@/registry/ui/popover/popover\";\n\nexport interface DropdownMenuProperties {\n  open?: boolean;\n  modal?: boolean;\n}\n\nexport type MenuItemWithProperties = HTMLElement & {\n  disabled?: boolean;\n  highlighted?: boolean;\n  value?: string;\n  checked?: boolean;\n};\n\nexport function isMenuItemElement(\n  element: HTMLElement,\n): element is MenuItemWithProperties {\n  const validTags = [\n    \"UI-DROPDOWN-MENU-ITEM\",\n    \"UI-DROPDOWN-MENU-CHECKBOX-ITEM\",\n    \"UI-DROPDOWN-MENU-RADIO-ITEM\",\n    \"UI-DROPDOWN-MENU-SUB-TRIGGER\",\n  ];\n  return validTags.includes(element.tagName);\n}\n\nconst isNode = (value: EventTarget | null): value is Node => {\n  return value instanceof Node;\n};\n\n@customElement(\"ui-dropdown-menu\")\nexport class DropdownMenu\n  extends TW(LitElement)\n  implements DropdownMenuProperties\n{\n  static styles = css`\n    :host {\n      display: contents;\n    }\n  `;\n\n  @property({ type: Boolean, reflect: true }) open = false;\n  @property({ type: Boolean }) modal = true;\n\n  @query(\"ui-dropdown-menu-trigger\") triggerElement?: HTMLElement;\n  @query(\"ui-popover\") popoverElement?: HTMLElement;\n\n  private clickAwayHandler = (e: MouseEvent) => {\n    if (!this.open) return;\n    const content = this.querySelector(\"ui-dropdown-menu-content\");\n    if (\n      isNode(e.target) &&\n      !this.contains(e.target) &&\n      (!content || !content.contains(e.target))\n    ) {\n      this.open = false;\n    }\n  };\n\n  override connectedCallback() {\n    super.connectedCallback();\n    this.addEventListener(\"trigger-click\", this.handleTriggerClick);\n    this.addEventListener(\"item-select\", this.handleItemSelect);\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    this.removeEventListener(\"trigger-click\", this.handleTriggerClick);\n    this.removeEventListener(\"item-select\", this.handleItemSelect);\n    document.removeEventListener(\"click\", this.clickAwayHandler);\n  }\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (changedProperties.has(\"open\")) {\n      if (this.open) {\n        setTimeout(\n          () => document.addEventListener(\"click\", this.clickAwayHandler),\n          0,\n        );\n        const content = this.querySelector(\"ui-dropdown-menu-content\");\n        if (content) {\n          setTimeout(() => {\n            const menu =\n              content.shadowRoot?.querySelector<HTMLElement>('[role=\"menu\"]');\n            menu?.focus();\n          }, 0);\n        }\n      } else {\n        document.removeEventListener(\"click\", this.clickAwayHandler);\n      }\n\n      this.dispatchEvent(\n        new CustomEvent(\"open-change\", {\n          detail: { open: this.open },\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  }\n\n  private handleTriggerClick = (e: Event) => {\n    e.stopPropagation();\n    this.open = !this.open;\n  };\n\n  private handleItemSelect = () => {\n    this.open = false;\n  };\n\n  override render() {\n    return html`\n      <ui-popover\n        .active=${this.open}\n        .anchor=${this.triggerElement}\n        placement=\"bottom-start\"\n        .distance=${8}\n        .flip=${true}\n        .shift=${true}\n      >\n        <slot name=\"trigger\" slot=\"anchor\"></slot>\n        <slot name=\"content\"></slot>\n      </ui-popover>\n    `;\n  }\n}\n\nexport interface DropdownMenuTriggerProperties {\n  disabled?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-trigger\")\nexport class DropdownMenuTrigger\n  extends TW(LitElement)\n  implements DropdownMenuTriggerProperties\n{\n  @property({ type: Boolean }) disabled = false;\n\n  private getExpanded() {\n    const menu = this.closest(\"ui-dropdown-menu\");\n    return menu?.open ? \"true\" : \"false\";\n  }\n\n  private handleClick = (e: Event) => {\n    if (!this.disabled) {\n      e.stopPropagation();\n      this.dispatchEvent(\n        new CustomEvent(\"trigger-click\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  override render() {\n    return html`\n      <button\n        type=\"button\"\n        role=\"combobox\"\n        aria-haspopup=\"menu\"\n        aria-expanded=${this.getExpanded()}\n        class=${cn(\n          \"flex h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors\",\n          \"hover:bg-accent hover:text-accent-foreground\",\n          \"focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          this.className,\n        )}\n        ?disabled=${this.disabled}\n        @click=${this.handleClick}\n      >\n        <slot></slot>\n      </button>\n    `;\n  }\n}\n\nexport interface DropdownMenuContentProperties {\n  align?: \"start\" | \"center\" | \"end\";\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  sideOffset?: number;\n  alignOffset?: number;\n  loop?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-content\")\nexport class DropdownMenuContent\n  extends TW(LitElement)\n  implements DropdownMenuContentProperties\n{\n  @property({ type: String }) align: \"start\" | \"center\" | \"end\" = \"start\";\n  @property({ type: String }) side: \"top\" | \"right\" | \"bottom\" | \"left\" =\n    \"bottom\";\n  @property({ type: Number }) sideOffset = 4;\n  @property({ type: Number }) alignOffset = 0;\n  @property({ type: Boolean }) loop = false;\n\n  @state() protected isOpen = false;\n  @state() private highlightedIndex = -1;\n  @state() private typeaheadString = \"\";\n  private typeaheadTimeout?: number;\n\n  @queryAssignedElements({ flatten: true })\n  private items!: HTMLElement[];\n\n  override connectedCallback() {\n    super.connectedCallback();\n    const menu = this.closest(\"ui-dropdown-menu\");\n    if (menu) {\n      this.isOpen = menu.open;\n      const observer = new MutationObserver(() => {\n        this.isOpen = menu.open;\n      });\n      observer.observe(menu, { attributes: true, attributeFilter: [\"open\"] });\n    }\n  }\n\n  private getNavigableItems(): MenuItemWithProperties[] {\n    return this.items.filter(\n      (item): item is MenuItemWithProperties =>\n        (item.tagName === \"UI-DROPDOWN-MENU-ITEM\" ||\n          item.tagName === \"UI-DROPDOWN-MENU-CHECKBOX-ITEM\" ||\n          item.tagName === \"UI-DROPDOWN-MENU-RADIO-ITEM\" ||\n          item.tagName === \"UI-DROPDOWN-MENU-SUB-TRIGGER\") &&\n        isMenuItemElement(item) &&\n        !item.disabled,\n    );\n  }\n\n  private handleKeyDown = (e: KeyboardEvent) => {\n    const navItems = this.getNavigableItems();\n    if (navItems.length === 0) return;\n\n    switch (e.key) {\n      case \"ArrowDown\":\n        e.preventDefault();\n        this.highlightedIndex = this.loop\n          ? (this.highlightedIndex + 1) % navItems.length\n          : Math.min(this.highlightedIndex + 1, navItems.length - 1);\n        this.updateHighlighted(navItems);\n        break;\n      case \"ArrowUp\":\n        e.preventDefault();\n        this.highlightedIndex = this.loop\n          ? (this.highlightedIndex - 1 + navItems.length) % navItems.length\n          : Math.max(this.highlightedIndex - 1, 0);\n        this.updateHighlighted(navItems);\n        break;\n      case \"Home\":\n        e.preventDefault();\n        this.highlightedIndex = 0;\n        this.updateHighlighted(navItems);\n        break;\n      case \"End\":\n        e.preventDefault();\n        this.highlightedIndex = navItems.length - 1;\n        this.updateHighlighted(navItems);\n        break;\n      case \"Enter\":\n      case \" \":\n        e.preventDefault();\n        if (this.highlightedIndex >= 0) {\n          navItems[this.highlightedIndex]?.click();\n        }\n        break;\n      case \"Escape\": {\n        e.preventDefault();\n        const menu = this.closest(\"ui-dropdown-menu\");\n        if (menu) menu.open = false;\n        break;\n      }\n      default:\n        if (e.key.length === 1) {\n          this.handleTypeahead(e.key, navItems);\n        }\n    }\n  };\n\n  private handleTypeahead(char: string, items: HTMLElement[]) {\n    clearTimeout(this.typeaheadTimeout);\n    this.typeaheadString += char.toLowerCase();\n\n    const matchIndex = items.findIndex((item) =>\n      item.textContent?.toLowerCase().startsWith(this.typeaheadString),\n    );\n\n    if (matchIndex >= 0) {\n      this.highlightedIndex = matchIndex;\n      this.updateHighlighted(items);\n    }\n\n    this.typeaheadTimeout = window.setTimeout(() => {\n      this.typeaheadString = \"\";\n    }, 500);\n  }\n\n  private updateHighlighted(items: MenuItemWithProperties[]) {\n    items.forEach((item, index) => {\n      item.highlighted = index === this.highlightedIndex;\n    });\n  }\n\n  override render() {\n    if (!this.isOpen) return nothing;\n\n    return html`\n      <div\n        role=\"menu\"\n        tabindex=\"0\"\n        aria-orientation=\"vertical\"\n        class=${cn(\n          \"min-w-[8rem] max-w-[20rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n          \"animate-in fade-in-80 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\",\n          this.className,\n        )}\n        data-side=${this.side}\n        @keydown=${this.handleKeyDown}\n      >\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface DropdownMenuItemProperties {\n  disabled?: boolean;\n  inset?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-item\")\nexport class DropdownMenuItem\n  extends TW(LitElement)\n  implements DropdownMenuItemProperties\n{\n  @property({ type: Boolean }) disabled = false;\n  @property({ type: Boolean }) inset = false;\n\n  @state() highlighted = false;\n\n  private handleClick = () => {\n    if (!this.disabled) {\n      this.dispatchEvent(\n        new CustomEvent(\"select\", {\n          detail: { value: this.textContent },\n          bubbles: true,\n          composed: true,\n        }),\n      );\n      this.dispatchEvent(\n        new CustomEvent(\"item-select\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  private handleMouseEnter = () => {\n    if (!this.disabled) {\n      this.highlighted = true;\n    }\n  };\n\n  private handleMouseLeave = () => {\n    this.highlighted = false;\n  };\n\n  override render() {\n    return html`\n      <div\n        role=\"menuitem\"\n        tabindex=\"-1\"\n        aria-disabled=${this.disabled}\n        class=${cn(\n          \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors\",\n          \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n          \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n          \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n          this.inset && \"pl-8\",\n          this.className,\n        )}\n        ?data-disabled=${this.disabled}\n        ?data-highlighted=${this.highlighted}\n        @click=${this.handleClick}\n        @mouseenter=${this.handleMouseEnter}\n        @mouseleave=${this.handleMouseLeave}\n      >\n        <slot></slot>\n        <span class=\"ml-auto text-xs\">\n          <slot name=\"shortcut\"></slot>\n        </span>\n      </div>\n    `;\n  }\n}\n\nexport interface DropdownMenuCheckboxItemProperties {\n  checked?: boolean;\n  disabled?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-checkbox-item\")\nexport class DropdownMenuCheckboxItem\n  extends TW(LitElement)\n  implements DropdownMenuCheckboxItemProperties\n{\n  @property({ type: Boolean }) checked = false;\n  @property({ type: Boolean }) disabled = false;\n\n  @state() highlighted = false;\n\n  private handleClick = () => {\n    if (!this.disabled) {\n      this.checked = !this.checked;\n      this.dispatchEvent(\n        new CustomEvent(\"checked-change\", {\n          detail: { checked: this.checked },\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  override render() {\n    return html`\n      <div\n        role=\"menuitemcheckbox\"\n        aria-checked=${this.checked}\n        tabindex=\"-1\"\n        aria-disabled=${this.disabled}\n        class=${cn(\n          \"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors\",\n          \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n          \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n          this.className,\n        )}\n        data-state=${this.checked ? \"checked\" : \"unchecked\"}\n        ?data-disabled=${this.disabled}\n        @click=${this.handleClick}\n      >\n        <span\n          class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\"\n        >\n          ${this.checked ? unsafeSVG(Check) : nothing}\n        </span>\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface DropdownMenuRadioGroupProperties {\n  value?: string;\n}\n\n@customElement(\"ui-dropdown-menu-radio-group\")\nexport class DropdownMenuRadioGroup\n  extends TW(LitElement)\n  implements DropdownMenuRadioGroupProperties\n{\n  @property({ type: String }) value = \"\";\n\n  override connectedCallback() {\n    super.connectedCallback();\n    this.addEventListener(\"radio-select\", this.handleRadioSelect);\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    this.removeEventListener(\"radio-select\", this.handleRadioSelect);\n  }\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (changedProperties.has(\"value\")) {\n      this.updateRadioItems();\n    }\n  }\n\n  private updateRadioItems() {\n    const items = this.querySelectorAll(\"ui-dropdown-menu-radio-item\");\n    items.forEach((item) => {\n      if (isMenuItemElement(item) && \"value\" in item) {\n        item.checked = item.value === this.value;\n      }\n    });\n  }\n\n  private handleRadioSelect = (e: Event) => {\n    if (e instanceof CustomEvent) {\n      e.stopPropagation();\n      this.value = e.detail.value;\n      this.dispatchEvent(\n        new CustomEvent(\"value-change\", {\n          detail: { value: this.value },\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  override render() {\n    return html`\n      <div role=\"group\">\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface DropdownMenuRadioItemProperties {\n  value?: string;\n  checked?: boolean;\n  disabled?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-radio-item\")\nexport class DropdownMenuRadioItem\n  extends TW(LitElement)\n  implements DropdownMenuRadioItemProperties\n{\n  @property({ type: String }) value = \"\";\n  @property({ type: Boolean }) checked = false;\n  @property({ type: Boolean }) disabled = false;\n\n  @state() highlighted = false;\n\n  private handleClick = () => {\n    if (!this.disabled) {\n      this.dispatchEvent(\n        new CustomEvent(\"radio-select\", {\n          detail: { value: this.value },\n          bubbles: true,\n          composed: true,\n        }),\n      );\n      this.dispatchEvent(\n        new CustomEvent(\"item-select\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  override render() {\n    return html`\n      <div\n        role=\"menuitemradio\"\n        aria-checked=${this.checked}\n        tabindex=\"-1\"\n        aria-disabled=${this.disabled}\n        class=${cn(\n          \"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors\",\n          \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n          \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n          this.className,\n        )}\n        ?data-disabled=${this.disabled}\n        @click=${this.handleClick}\n      >\n        <span\n          class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\"\n        >\n          ${this.checked ? unsafeSVG(Circle) : nothing}\n        </span>\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface DropdownMenuSubProperties {\n  open?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-sub\")\nexport class DropdownMenuSub\n  extends TW(LitElement)\n  implements DropdownMenuSubProperties\n{\n  static styles = css`\n    :host {\n      display: contents;\n    }\n  `;\n\n  @property({ type: Boolean, reflect: true }) open = false;\n\n  @query(\"ui-dropdown-menu-sub-trigger\") triggerElement?: HTMLElement;\n\n  private hoverTimeout?: number;\n  private closeTimeout?: number;\n\n  override connectedCallback() {\n    super.connectedCallback();\n    this.addEventListener(\"sub-trigger-click\", this.handleTriggerClick);\n    this.addEventListener(\"sub-trigger-mouseenter\", this.handleTriggerHover);\n    this.addEventListener(\"sub-trigger-mouseleave\", this.handleTriggerLeave);\n    this.addEventListener(\"sub-content-mouseenter\", this.handleContentEnter);\n    this.addEventListener(\"sub-content-mouseleave\", this.handleContentLeave);\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    clearTimeout(this.hoverTimeout);\n    clearTimeout(this.closeTimeout);\n    this.removeEventListener(\"sub-trigger-click\", this.handleTriggerClick);\n    this.removeEventListener(\"sub-trigger-mouseenter\", this.handleTriggerHover);\n    this.removeEventListener(\"sub-trigger-mouseleave\", this.handleTriggerLeave);\n    this.removeEventListener(\"sub-content-mouseenter\", this.handleContentEnter);\n    this.removeEventListener(\"sub-content-mouseleave\", this.handleContentLeave);\n  }\n\n  private handleTriggerClick = (e: Event) => {\n    e.stopPropagation();\n    this.open = !this.open;\n  };\n\n  private handleTriggerHover = () => {\n    clearTimeout(this.hoverTimeout);\n    clearTimeout(this.closeTimeout);\n    this.hoverTimeout = window.setTimeout(() => {\n      this.open = true;\n    }, 200);\n  };\n\n  private handleTriggerLeave = () => {\n    clearTimeout(this.hoverTimeout);\n    this.closeTimeout = window.setTimeout(() => {\n      this.open = false;\n    }, 300);\n  };\n\n  private handleContentEnter = () => {\n    clearTimeout(this.closeTimeout);\n  };\n\n  private handleContentLeave = () => {\n    clearTimeout(this.closeTimeout);\n    this.closeTimeout = window.setTimeout(() => {\n      this.open = false;\n    }, 300);\n  };\n\n  override render() {\n    return html`\n      <ui-popover\n        .active=${this.open}\n        .anchor=${this.triggerElement}\n        placement=\"right-start\"\n        strategy=\"fixed\"\n        .distance=${4}\n        .skidding=${-4}\n        .flip=${true}\n        .shift=${true}\n      >\n        <slot name=\"trigger\" slot=\"anchor\"></slot>\n        <slot name=\"content\"></slot>\n      </ui-popover>\n    `;\n  }\n}\n\nexport interface DropdownMenuSubTriggerProperties {\n  disabled?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-sub-trigger\")\nexport class DropdownMenuSubTrigger\n  extends TW(LitElement)\n  implements DropdownMenuSubTriggerProperties\n{\n  @property({ type: Boolean }) disabled = false;\n\n  @state() highlighted = false;\n\n  private getSubMenuOpen() {\n    const sub = this.closest(\"ui-dropdown-menu-sub\");\n    return sub?.open ? \"true\" : \"false\";\n  }\n\n  private handleClick = (e: Event) => {\n    if (!this.disabled) {\n      e.stopPropagation();\n      this.dispatchEvent(\n        new CustomEvent(\"sub-trigger-click\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  private handleMouseEnter = () => {\n    if (!this.disabled) {\n      this.highlighted = true;\n      this.dispatchEvent(\n        new CustomEvent(\"sub-trigger-mouseenter\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  private handleMouseLeave = () => {\n    this.highlighted = false;\n    if (!this.disabled) {\n      this.dispatchEvent(\n        new CustomEvent(\"sub-trigger-mouseleave\", {\n          bubbles: true,\n          composed: true,\n        }),\n      );\n    }\n  };\n\n  override render() {\n    return html`\n      <div\n        role=\"menuitem\"\n        aria-haspopup=\"menu\"\n        aria-expanded=${this.getSubMenuOpen()}\n        tabindex=\"-1\"\n        aria-disabled=${this.disabled}\n        class=${cn(\n          \"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors\",\n          \"hover:bg-accent focus:bg-accent data-[state=open]:bg-accent\",\n          \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n          this.className,\n        )}\n        ?data-disabled=${this.disabled}\n        @click=${this.handleClick}\n        @mouseenter=${this.handleMouseEnter}\n        @mouseleave=${this.handleMouseLeave}\n      >\n        <slot></slot>\n        <span class=\"ml-auto\" aria-hidden=\"true\">\n          ${unsafeSVG(ChevronRight)}\n        </span>\n      </div>\n    `;\n  }\n}\n\n@customElement(\"ui-dropdown-menu-sub-content\")\nexport class DropdownMenuSubContent extends DropdownMenuContent {\n  override connectedCallback() {\n    super.connectedCallback();\n    const sub = this.closest(\"ui-dropdown-menu-sub\");\n    if (sub) {\n      const observer = new MutationObserver(() => {\n        this.isOpen = sub.open;\n      });\n      observer.observe(sub, { attributes: true, attributeFilter: [\"open\"] });\n      this.isOpen = sub.open;\n    }\n  }\n\n  private handleMouseEnter = () => {\n    this.dispatchEvent(\n      new CustomEvent(\"sub-content-mouseenter\", {\n        bubbles: true,\n        composed: true,\n      }),\n    );\n  };\n\n  private handleMouseLeave = () => {\n    this.dispatchEvent(\n      new CustomEvent(\"sub-content-mouseleave\", {\n        bubbles: true,\n        composed: true,\n      }),\n    );\n  };\n\n  override render() {\n    const parentRender = super.render();\n    if (parentRender === nothing) return nothing;\n\n    return html`\n      <div\n        @mouseenter=${this.handleMouseEnter}\n        @mouseleave=${this.handleMouseLeave}\n      >\n        ${parentRender}\n      </div>\n    `;\n  }\n}\n\n@customElement(\"ui-dropdown-menu-separator\")\nexport class DropdownMenuSeparator extends TW(LitElement) {\n  override render() {\n    return html`\n      <div\n        role=\"separator\"\n        aria-orientation=\"horizontal\"\n        class=\"-mx-1 my-1 h-px bg-muted\"\n      ></div>\n    `;\n  }\n}\n\nexport interface DropdownMenuLabelProperties {\n  inset?: boolean;\n}\n\n@customElement(\"ui-dropdown-menu-label\")\nexport class DropdownMenuLabel\n  extends TW(LitElement)\n  implements DropdownMenuLabelProperties\n{\n  @property({ type: Boolean }) inset = false;\n\n  override render() {\n    return html`\n      <div\n        class=${cn(\n          \"px-2 py-1.5 text-sm font-semibold\",\n          this.inset && \"pl-8\",\n          this.className,\n        )}\n      >\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\n@customElement(\"ui-dropdown-menu-group\")\nexport class DropdownMenuGroup extends TW(LitElement) {\n  override render() {\n    return html`\n      <div role=\"group\">\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\n@customElement(\"ui-dropdown-menu-shortcut\")\nexport class DropdownMenuShortcut extends TW(LitElement) {\n  override render() {\n    return html`\n      <span class=\"ml-auto text-xs tracking-widest text-muted-foreground\">\n        <slot></slot>\n      </span>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    \"ui-dropdown-menu\": DropdownMenu;\n    \"ui-dropdown-menu-trigger\": DropdownMenuTrigger;\n    \"ui-dropdown-menu-content\": DropdownMenuContent;\n    \"ui-dropdown-menu-item\": DropdownMenuItem;\n    \"ui-dropdown-menu-checkbox-item\": DropdownMenuCheckboxItem;\n    \"ui-dropdown-menu-radio-group\": DropdownMenuRadioGroup;\n    \"ui-dropdown-menu-radio-item\": DropdownMenuRadioItem;\n    \"ui-dropdown-menu-sub\": DropdownMenuSub;\n    \"ui-dropdown-menu-sub-trigger\": DropdownMenuSubTrigger;\n    \"ui-dropdown-menu-sub-content\": DropdownMenuSubContent;\n    \"ui-dropdown-menu-separator\": DropdownMenuSeparator;\n    \"ui-dropdown-menu-label\": DropdownMenuLabel;\n    \"ui-dropdown-menu-group\": DropdownMenuGroup;\n    \"ui-dropdown-menu-shortcut\": DropdownMenuShortcut;\n  }\n}\n"},{"path":"registry/ui/dropdown-menu/dropdown-menu.stories.ts","type":"registry:ui","content":"import \"./dropdown-menu\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\n\nconst meta: Meta = {\n  title: \"ui/Dropdown Menu\",\n  component: \"ui-dropdown-menu\",\n  tags: [\"autodocs\"],\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\"> Open </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-item>Profile</ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>Settings</ui-dropdown-menu-item>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-item>Logout</ui-dropdown-menu-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n  parameters: {\n    layout: \"centered\",\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Default: Story = {};\n\nexport const WithCheckboxes: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\">\n        View Options\n      </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-label>Appearance</ui-dropdown-menu-label>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-checkbox-item checked>\n          Show Toolbar\n        </ui-dropdown-menu-checkbox-item>\n        <ui-dropdown-menu-checkbox-item>\n          Show Sidebar\n        </ui-dropdown-menu-checkbox-item>\n        <ui-dropdown-menu-checkbox-item>\n          Show Footer\n        </ui-dropdown-menu-checkbox-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n\nexport const WithRadioGroup: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\">\n        Text Size\n      </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-label>Font Size</ui-dropdown-menu-label>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-radio-group value=\"medium\">\n          <ui-dropdown-menu-radio-item value=\"small\"\n            >Small</ui-dropdown-menu-radio-item\n          >\n          <ui-dropdown-menu-radio-item value=\"medium\"\n            >Medium</ui-dropdown-menu-radio-item\n          >\n          <ui-dropdown-menu-radio-item value=\"large\"\n            >Large</ui-dropdown-menu-radio-item\n          >\n        </ui-dropdown-menu-radio-group>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n\nexport const WithSubmenu: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\">\n        File Menu\n      </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-item>\n          New File\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘N</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n\n        <ui-dropdown-menu-sub>\n          <ui-dropdown-menu-sub-trigger slot=\"trigger\">\n            Open Recent\n          </ui-dropdown-menu-sub-trigger>\n\n          <ui-dropdown-menu-sub-content slot=\"content\">\n            <ui-dropdown-menu-item>document.txt</ui-dropdown-menu-item>\n            <ui-dropdown-menu-item>image.png</ui-dropdown-menu-item>\n            <ui-dropdown-menu-item>presentation.pdf</ui-dropdown-menu-item>\n          </ui-dropdown-menu-sub-content>\n        </ui-dropdown-menu-sub>\n\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-item>\n          Save\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘S</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n\nexport const WithShortcuts: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\"> Edit </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-item>\n          Undo\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘Z</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>\n          Redo\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘⇧Z</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-item>\n          Cut\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘X</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>\n          Copy\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘C</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>\n          Paste\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⌘V</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n\nexport const WithDisabledItems: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\">\n        Actions\n      </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-item>Edit</ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>Duplicate</ui-dropdown-menu-item>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n        <ui-dropdown-menu-item disabled>Archive</ui-dropdown-menu-item>\n        <ui-dropdown-menu-item>Delete</ui-dropdown-menu-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n\nexport const Complex: Story = {\n  render: () => html`\n    <ui-dropdown-menu>\n      <ui-dropdown-menu-trigger slot=\"trigger\">\n        Account\n      </ui-dropdown-menu-trigger>\n\n      <ui-dropdown-menu-content slot=\"content\">\n        <ui-dropdown-menu-label>My Account</ui-dropdown-menu-label>\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n\n        <ui-dropdown-menu-group>\n          <ui-dropdown-menu-item>\n            Profile\n            <ui-dropdown-menu-shortcut slot=\"shortcut\"\n              >⌘P</ui-dropdown-menu-shortcut\n            >\n          </ui-dropdown-menu-item>\n          <ui-dropdown-menu-item>\n            Billing\n            <ui-dropdown-menu-shortcut slot=\"shortcut\"\n              >⌘B</ui-dropdown-menu-shortcut\n            >\n          </ui-dropdown-menu-item>\n          <ui-dropdown-menu-item>\n            Settings\n            <ui-dropdown-menu-shortcut slot=\"shortcut\"\n              >⌘S</ui-dropdown-menu-shortcut\n            >\n          </ui-dropdown-menu-item>\n        </ui-dropdown-menu-group>\n\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n\n        <ui-dropdown-menu-sub>\n          <ui-dropdown-menu-sub-trigger slot=\"trigger\">\n            Invite users\n          </ui-dropdown-menu-sub-trigger>\n\n          <ui-dropdown-menu-sub-content slot=\"content\">\n            <ui-dropdown-menu-item>Email</ui-dropdown-menu-item>\n            <ui-dropdown-menu-item>Message</ui-dropdown-menu-item>\n            <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n            <ui-dropdown-menu-item>More...</ui-dropdown-menu-item>\n          </ui-dropdown-menu-sub-content>\n        </ui-dropdown-menu-sub>\n\n        <ui-dropdown-menu-separator></ui-dropdown-menu-separator>\n\n        <ui-dropdown-menu-item>\n          Log out\n          <ui-dropdown-menu-shortcut slot=\"shortcut\"\n            >⇧⌘Q</ui-dropdown-menu-shortcut\n          >\n        </ui-dropdown-menu-item>\n      </ui-dropdown-menu-content>\n    </ui-dropdown-menu>\n  `,\n};\n"}]}