{"name":"menubar","type":"registry:component","title":"Menubar","description":"A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. Features horizontal keyboard navigation and accessible ARIA attributes.","categories":["ui","navigation","menu","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","registryDependencies":["@lit/popover"],"dependencies":["lucide-static","@floating-ui/dom"],"files":[{"path":"registry/ui/menubar/menubar.ts","type":"registry:ui","content":"import { TW } from \"@/registry/lib/tailwindMixin\";\nimport { cn } from \"@/registry/lib/utils\";\nimport \"@/registry/ui/popover/popover\";\nimport { 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\";\n\nexport interface MenubarProperties {\n  value?: string;\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-MENUBAR-ITEM\",\n    \"UI-MENUBAR-CHECKBOX-ITEM\",\n    \"UI-MENUBAR-RADIO-ITEM\",\n    \"UI-MENUBAR-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-menubar\")\nexport class Menubar extends TW(LitElement) implements MenubarProperties {\n  @property({ type: String }) value = \"\";\n\n  @queryAssignedElements({ selector: \"ui-menubar-menu\" })\n  private menus!: MenubarMenu[];\n\n  private clickAwayHandler = (e: MouseEvent) => {\n    if (isNode(e.target) && !this.contains(e.target)) {\n      this.value = \"\";\n    }\n  };\n\n  override connectedCallback() {\n    super.connectedCallback();\n    this.addEventListener(\n      \"menubar-trigger-click\",\n      this.handleTriggerClick as EventListener,\n    );\n    this.addEventListener(\"menubar-item-select\", this.handleItemSelect);\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    this.removeEventListener(\n      \"menubar-trigger-click\",\n      this.handleTriggerClick as EventListener,\n    );\n    this.removeEventListener(\"menubar-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(\"value\")) {\n      this.updateMenuStates();\n      this.updateRovingTabindex();\n\n      if (this.value !== \"\") {\n        setTimeout(\n          () => document.addEventListener(\"click\", this.clickAwayHandler),\n          0,\n        );\n      } else {\n        document.removeEventListener(\"click\", this.clickAwayHandler);\n      }\n\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  private handleTriggerClick = (e: CustomEvent) => {\n    if (!(e.target instanceof HTMLElement)) return;\n    const trigger = e.target;\n    const menu = trigger.closest(\"ui-menubar-menu\");\n    if (!menu) return;\n\n    if (this.value === menu.value) {\n      this.value = \"\";\n    } else {\n      this.value = menu.value;\n    }\n  };\n\n  private handleItemSelect = () => {\n    this.value = \"\";\n  };\n\n  private handleKeyDown = (e: KeyboardEvent) => {\n    const menus = this.menus.filter(\n      (m) => !(\"disabled\" in m && (m as { disabled?: boolean }).disabled),\n    );\n    const currentIndex = menus.findIndex((m) => m.value === this.value);\n\n    switch (e.key) {\n      case \"ArrowRight\": {\n        e.preventDefault();\n        const nextIndex = (currentIndex + 1) % menus.length;\n        this.focusMenu(menus[nextIndex]);\n        if (this.value !== \"\") {\n          this.value = menus[nextIndex].value;\n        }\n        break;\n      }\n      case \"ArrowLeft\": {\n        e.preventDefault();\n        const prevIndex = (currentIndex - 1 + menus.length) % menus.length;\n        this.focusMenu(menus[prevIndex]);\n        if (this.value !== \"\") {\n          this.value = menus[prevIndex].value;\n        }\n        break;\n      }\n      case \"Enter\":\n        if (this.value === \"\" && currentIndex >= 0) {\n          e.preventDefault();\n          this.value = menus[currentIndex].value;\n        } else if (this.value !== \"\") {\n          e.preventDefault();\n          this.value = \"\";\n          if (currentIndex >= 0) {\n            this.focusMenu(menus[currentIndex]);\n          }\n        }\n        break;\n      case \"Home\":\n        e.preventDefault();\n        this.focusMenu(menus[0]);\n        break;\n      case \"End\":\n        e.preventDefault();\n        this.focusMenu(menus[menus.length - 1]);\n        break;\n      case \"Escape\":\n        if (this.value !== \"\") {\n          e.preventDefault();\n          const currentMenu = menus.find((m) => m.value === this.value);\n          this.value = \"\";\n          if (currentMenu) {\n            this.focusMenu(currentMenu);\n          }\n        }\n        break;\n    }\n  };\n\n  private focusMenu(menu: MenubarMenu) {\n    const trigger = menu.querySelector(\"ui-menubar-trigger\");\n    const button = trigger?.shadowRoot?.querySelector(\"button\");\n    button?.focus();\n  }\n\n  private updateMenuStates() {\n    this.menus.forEach((menu) => {\n      menu.open = menu.value === this.value;\n    });\n  }\n\n  private updateRovingTabindex() {\n    const menus = this.menus.filter(\n      (m) => !(\"disabled\" in m && (m as { disabled?: boolean }).disabled),\n    );\n    const openIndex = menus.findIndex((m) => m.open);\n    const focusIndex = openIndex >= 0 ? openIndex : 0;\n\n    menus.forEach((menu, index) => {\n      const trigger = menu.querySelector(\"ui-menubar-trigger\");\n      const button = trigger?.shadowRoot?.querySelector(\"button\");\n      if (button) {\n        button.tabIndex = index === focusIndex ? 0 : -1;\n      }\n    });\n  }\n\n  override render() {\n    return html`\n      <div\n        role=\"menubar\"\n        aria-orientation=\"horizontal\"\n        class=${cn(\n          \"flex h-10 items-center space-x-1 rounded-md border bg-background p-1\",\n          this.className,\n        )}\n        @keydown=${this.handleKeyDown}\n      >\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface MenubarMenuProperties {\n  value: string;\n  open?: boolean;\n}\n\n@customElement(\"ui-menubar-menu\")\nexport class MenubarMenu\n  extends TW(LitElement)\n  implements MenubarMenuProperties\n{\n  static styles = css`\n    :host {\n      display: contents;\n    }\n  `;\n\n  @property({ type: String }) value = \"\";\n  @property({ type: Boolean, reflect: true }) open = false;\n\n  @query(\"ui-menubar-trigger\") triggerElement?: HTMLElement;\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (changedProperties.has(\"open\")) {\n      const trigger = this.querySelector(\"ui-menubar-trigger\");\n      if (trigger) {\n        trigger.active = this.open;\n      }\n\n      if (this.open) {\n        setTimeout(() => {\n          const content = this.querySelector(\"ui-menubar-content\");\n          const menu =\n            content?.shadowRoot?.querySelector<HTMLElement>('[role=\"menu\"]');\n          if (menu) {\n            menu.focus();\n            const contentInstance = content as MenubarContent;\n            if (contentInstance) {\n              contentInstance.highlightedIndex = 0;\n              const items = contentInstance.getNavigableItems();\n              contentInstance.updateHighlighted(items);\n            }\n          }\n        }, 0);\n      }\n    }\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 MenubarTriggerProperties {\n  disabled?: boolean;\n  active?: boolean;\n}\n\n@customElement(\"ui-menubar-trigger\")\nexport class MenubarTrigger\n  extends TW(LitElement)\n  implements MenubarTriggerProperties\n{\n  @property({ type: Boolean }) disabled = false;\n  @property({ type: Boolean }) active = false;\n\n  private handleClick = (e: Event) => {\n    if (!this.disabled) {\n      e.stopPropagation();\n      this.dispatchEvent(\n        new CustomEvent(\"menubar-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=\"menuitem\"\n        aria-haspopup=\"menu\"\n        aria-expanded=${this.active}\n        class=${cn(\n          \"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none\",\n          \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n          \"focus:bg-accent focus:text-accent-foreground\",\n          \"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          this.className,\n        )}\n        data-state=${this.active ? \"open\" : \"closed\"}\n        ?disabled=${this.disabled}\n        @click=${this.handleClick}\n      >\n        <slot></slot>\n      </button>\n    `;\n  }\n}\n\nexport interface MenubarContentProperties {\n  align?: \"start\" | \"center\" | \"end\";\n  sideOffset?: number;\n  alignOffset?: number;\n  loop?: boolean;\n}\n\n@customElement(\"ui-menubar-content\")\nexport class MenubarContent\n  extends TW(LitElement)\n  implements MenubarContentProperties\n{\n  @property({ type: String }) align: \"start\" | \"center\" | \"end\" = \"start\";\n  @property({ type: Number }) sideOffset = 8;\n  @property({ type: Number }) alignOffset = 0;\n  @property({ type: Boolean }) loop = false;\n\n  @state() protected isOpen = false;\n  @state() public highlightedIndex = -1;\n  @state() protected typeaheadString = \"\";\n  protected typeaheadTimeout?: number;\n\n  @queryAssignedElements({ flatten: true })\n  protected items!: HTMLElement[];\n\n  override connectedCallback() {\n    super.connectedCallback();\n    const menu = this.closest(\"ui-menubar-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  public getNavigableItems(): MenuItemWithProperties[] {\n    const navigable: MenuItemWithProperties[] = [];\n    for (const item of this.items) {\n      if (isMenuItemElement(item)) {\n        navigable.push(item);\n      } else if (item.tagName === \"UI-MENUBAR-SUB\") {\n        const trigger = item.querySelector(\"ui-menubar-sub-trigger\");\n        if (trigger && isMenuItemElement(trigger)) {\n          navigable.push(trigger);\n        }\n      }\n    }\n    return navigable;\n  }\n\n  protected handleKeyDown(e: KeyboardEvent) {\n    const menubar = this.closest(\"ui-menubar\");\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        e.preventDefault();\n        e.stopPropagation();\n        if (this.highlightedIndex >= 0) {\n          const highlightedItem = navItems[this.highlightedIndex];\n          if (\n            highlightedItem &&\n            highlightedItem.tagName === \"UI-MENUBAR-SUB-TRIGGER\"\n          ) {\n            const sub = highlightedItem.closest(\"ui-menubar-sub\");\n            if (sub) {\n              sub.open = true;\n              setTimeout(() => {\n                const subContent = sub.querySelector(\"ui-menubar-sub-content\");\n                const subMenu =\n                  subContent?.shadowRoot?.querySelector<HTMLElement>(\n                    '[role=\"menu\"]',\n                  );\n                if (subMenu) {\n                  subMenu.focus();\n                  const subContentInstance = subContent as MenubarSubContent;\n                  if (subContentInstance) {\n                    subContentInstance.highlightedIndex = 0;\n                    const subItems = subContentInstance.getNavigableItems();\n                    subContentInstance.updateHighlighted(subItems);\n                  }\n                }\n              }, 0);\n              return;\n            }\n          }\n          navItems[this.highlightedIndex]?.click();\n        }\n        if (menubar) menubar.value = \"\";\n        break;\n      }\n      case \" \": {\n        e.preventDefault();\n        if (this.highlightedIndex >= 0) {\n          navItems[this.highlightedIndex]?.click();\n        }\n        if (menubar) menubar.value = \"\";\n        break;\n      }\n      case \"Escape\": {\n        e.preventDefault();\n        e.stopPropagation();\n        if (menubar) {\n          menubar.value = \"\";\n          const menu = this.closest(\"ui-menubar-menu\");\n          if (menu) {\n            const trigger = menu.querySelector(\"ui-menubar-trigger\");\n            const button = trigger?.shadowRoot?.querySelector(\"button\");\n            button?.focus();\n          }\n        }\n        break;\n      }\n      default:\n        if (e.key.length === 1) {\n          this.handleTypeahead(e.key, navItems);\n        }\n    }\n  }\n\n  protected 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  public 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\",\n          this.className,\n        )}\n        data-side=\"bottom\"\n        @keydown=${this.handleKeyDown}\n      >\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface MenubarItemProperties {\n  disabled?: boolean;\n  inset?: boolean;\n}\n\n@customElement(\"ui-menubar-item\")\nexport class MenubarItem\n  extends TW(LitElement)\n  implements MenubarItemProperties\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(\"menubar-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 MenubarCheckboxItemProperties {\n  checked?: boolean;\n  disabled?: boolean;\n}\n\n@customElement(\"ui-menubar-checkbox-item\")\nexport class MenubarCheckboxItem\n  extends TW(LitElement)\n  implements MenubarCheckboxItemProperties\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 MenubarRadioGroupProperties {\n  value?: string;\n}\n\n@customElement(\"ui-menubar-radio-group\")\nexport class MenubarRadioGroup\n  extends TW(LitElement)\n  implements MenubarRadioGroupProperties\n{\n  @property({ type: String }) value = \"\";\n\n  override connectedCallback() {\n    super.connectedCallback();\n    this.addEventListener(\n      \"radio-select\",\n      this.handleRadioSelect as EventListener,\n    );\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    this.removeEventListener(\n      \"radio-select\",\n      this.handleRadioSelect as EventListener,\n    );\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-menubar-radio-item\");\n    items.forEach((item) => {\n      if (\"value\" in item && typeof item.value === \"string\") {\n        item.checked = item.value === this.value;\n      }\n    });\n  }\n\n  private handleRadioSelect = (e: Event) => {\n    if (!(e instanceof CustomEvent)) return;\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  override render() {\n    return html`\n      <div role=\"group\">\n        <slot></slot>\n      </div>\n    `;\n  }\n}\n\nexport interface MenubarRadioItemProperties {\n  value?: string;\n  checked?: boolean;\n  disabled?: boolean;\n}\n\n@customElement(\"ui-menubar-radio-item\")\nexport class MenubarRadioItem\n  extends TW(LitElement)\n  implements MenubarRadioItemProperties\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(\"menubar-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 MenubarSubProperties {\n  open?: boolean;\n}\n\n@customElement(\"ui-menubar-sub\")\nexport class MenubarSub extends TW(LitElement) implements MenubarSubProperties {\n  static styles = css`\n    :host {\n      display: contents;\n    }\n  `;\n\n  @property({ type: Boolean, reflect: true }) open = false;\n\n  @query(\"ui-menubar-sub-trigger\") triggerElement?: HTMLElement;\n\n  override connectedCallback() {\n    super.connectedCallback();\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\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 MenubarSubTriggerProperties {\n  disabled?: boolean;\n}\n\n@customElement(\"ui-menubar-sub-trigger\")\nexport class MenubarSubTrigger\n  extends TW(LitElement)\n  implements MenubarSubTriggerProperties\n{\n  @property({ type: Boolean }) disabled = false;\n\n  @state() highlighted = false;\n\n  private getSubMenuOpen() {\n    const sub = this.closest(\"ui-menubar-sub\");\n    return sub?.open ? \"true\" : \"false\";\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        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          \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n          this.className,\n        )}\n        ?data-disabled=${this.disabled}\n        ?data-highlighted=${this.highlighted}\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-menubar-sub-content\")\nexport class MenubarSubContent extends MenubarContent {\n  override connectedCallback() {\n    super.connectedCallback();\n    const sub = this.closest(\"ui-menubar-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  protected override 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        e.stopPropagation();\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        e.stopPropagation();\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 \"ArrowLeft\": {\n        e.preventDefault();\n        e.stopPropagation();\n        const sub = this.closest(\"ui-menubar-sub\");\n        if (sub) {\n          sub.open = false;\n          setTimeout(() => {\n            const trigger = sub.querySelector(\"ui-menubar-sub-trigger\");\n            const triggerDiv = trigger?.shadowRoot?.querySelector(\"div\");\n            if (triggerDiv instanceof HTMLElement) {\n              triggerDiv.focus();\n              if (trigger) {\n                trigger.highlighted = true;\n              }\n            }\n            const parentContent = sub.closest(\"ui-menubar-content\");\n            const parentMenu =\n              parentContent?.shadowRoot?.querySelector<HTMLElement>(\n                '[role=\"menu\"]',\n              );\n            parentMenu?.focus();\n          }, 0);\n        }\n        break;\n      }\n      case \"Home\":\n        e.preventDefault();\n        e.stopPropagation();\n        this.highlightedIndex = 0;\n        this.updateHighlighted(navItems);\n        break;\n      case \"End\":\n        e.preventDefault();\n        e.stopPropagation();\n        this.highlightedIndex = navItems.length - 1;\n        this.updateHighlighted(navItems);\n        break;\n      case \"Enter\": {\n        e.preventDefault();\n        e.stopPropagation();\n        if (this.highlightedIndex >= 0) {\n          navItems[this.highlightedIndex]?.click();\n        }\n        const menubar = this.closest(\"ui-menubar\");\n        if (menubar) menubar.value = \"\";\n        break;\n      }\n      case \" \": {\n        e.preventDefault();\n        e.stopPropagation();\n        if (this.highlightedIndex >= 0) {\n          navItems[this.highlightedIndex]?.click();\n        }\n        const menubar = this.closest(\"ui-menubar\");\n        if (menubar) menubar.value = \"\";\n        break;\n      }\n      case \"Escape\": {\n        e.preventDefault();\n        e.stopPropagation();\n        const menubar = this.closest(\"ui-menubar\");\n        if (menubar) {\n          menubar.value = \"\";\n          const menu = this.closest(\"ui-menubar-menu\");\n          if (menu) {\n            const trigger = menu.querySelector(\"ui-menubar-trigger\");\n            const button = trigger?.shadowRoot?.querySelector(\"button\");\n            button?.focus();\n          }\n        }\n        break;\n      }\n      default:\n        if (e.key.length === 1) {\n          this.handleTypeahead(e.key, navItems);\n        }\n    }\n  }\n\n  override render() {\n    if (!this.isOpen) return nothing;\n\n    return html`\n      <div\n        @mouseenter=${this.handleMouseEnter}\n        @mouseleave=${this.handleMouseLeave}\n      >\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\",\n            this.className,\n          )}\n          data-side=\"bottom\"\n          @keydown=${this.handleKeyDown}\n        >\n          <slot></slot>\n        </div>\n      </div>\n    `;\n  }\n}\n\n@customElement(\"ui-menubar-separator\")\nexport class MenubarSeparator 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 MenubarLabelProperties {\n  inset?: boolean;\n}\n\n@customElement(\"ui-menubar-label\")\nexport class MenubarLabel\n  extends TW(LitElement)\n  implements MenubarLabelProperties\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-menubar-group\")\nexport class MenubarGroup 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-menubar-shortcut\")\nexport class MenubarShortcut 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-menubar\": Menubar;\n    \"ui-menubar-menu\": MenubarMenu;\n    \"ui-menubar-trigger\": MenubarTrigger;\n    \"ui-menubar-content\": MenubarContent;\n    \"ui-menubar-item\": MenubarItem;\n    \"ui-menubar-checkbox-item\": MenubarCheckboxItem;\n    \"ui-menubar-radio-group\": MenubarRadioGroup;\n    \"ui-menubar-radio-item\": MenubarRadioItem;\n    \"ui-menubar-sub\": MenubarSub;\n    \"ui-menubar-sub-trigger\": MenubarSubTrigger;\n    \"ui-menubar-sub-content\": MenubarSubContent;\n    \"ui-menubar-separator\": MenubarSeparator;\n    \"ui-menubar-label\": MenubarLabel;\n    \"ui-menubar-group\": MenubarGroup;\n    \"ui-menubar-shortcut\": MenubarShortcut;\n  }\n}\n"},{"path":"registry/ui/menubar/menubar.stories.ts","type":"registry:ui","content":"import \"./menubar\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\n\nconst meta: Meta = {\n  title: \"ui/Menubar\",\n  component: \"ui-menubar\",\n  tags: [\"autodocs\"],\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"file\">\n        <ui-menubar-trigger slot=\"trigger\">File</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>\n            New File\n            <ui-menubar-shortcut slot=\"shortcut\">⌘N</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>Open</ui-menubar-item>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>Save</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"edit\">\n        <ui-menubar-trigger slot=\"trigger\">Edit</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>Undo</ui-menubar-item>\n          <ui-menubar-item>Redo</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"view\">\n        <ui-menubar-trigger slot=\"trigger\">View</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-checkbox-item checked>\n            Show Toolbar\n          </ui-menubar-checkbox-item>\n          <ui-menubar-checkbox-item> Show Sidebar </ui-menubar-checkbox-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Default: Story = {};\n\nexport const WithCheckboxes: Story = {\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"view\">\n        <ui-menubar-trigger slot=\"trigger\">View</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-label>Appearance</ui-menubar-label>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-checkbox-item checked>\n            Show Toolbar\n          </ui-menubar-checkbox-item>\n          <ui-menubar-checkbox-item> Show Sidebar </ui-menubar-checkbox-item>\n          <ui-menubar-checkbox-item> Show Footer </ui-menubar-checkbox-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"window\">\n        <ui-menubar-trigger slot=\"trigger\">Window</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-checkbox-item> Always on Top </ui-menubar-checkbox-item>\n          <ui-menubar-checkbox-item checked>\n            Full Screen\n          </ui-menubar-checkbox-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n\nexport const WithRadioGroups: Story = {\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"profiles\">\n        <ui-menubar-trigger slot=\"trigger\">Profiles</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-radio-group value=\"andy\">\n            <ui-menubar-radio-item value=\"andy\">Andy</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"benoit\">Benoit</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"luis\">Luis</ui-menubar-radio-item>\n          </ui-menubar-radio-group>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>Edit...</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"theme\">\n        <ui-menubar-trigger slot=\"trigger\">Theme</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-radio-group value=\"light\">\n            <ui-menubar-radio-item value=\"light\">Light</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"dark\">Dark</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"system\">System</ui-menubar-radio-item>\n          </ui-menubar-radio-group>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n\nexport const WithSubmenu: Story = {\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"file\">\n        <ui-menubar-trigger slot=\"trigger\">File</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>New File</ui-menubar-item>\n\n          <ui-menubar-sub>\n            <ui-menubar-sub-trigger slot=\"trigger\">\n              Open Recent\n            </ui-menubar-sub-trigger>\n            <ui-menubar-sub-content slot=\"content\">\n              <ui-menubar-item>document.txt</ui-menubar-item>\n              <ui-menubar-item>image.png</ui-menubar-item>\n              <ui-menubar-item>presentation.pdf</ui-menubar-item>\n            </ui-menubar-sub-content>\n          </ui-menubar-sub>\n\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>Save</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"edit\">\n        <ui-menubar-trigger slot=\"trigger\">Edit</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>Undo</ui-menubar-item>\n          <ui-menubar-item>Redo</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n\nexport const WithShortcuts: Story = {\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"file\">\n        <ui-menubar-trigger slot=\"trigger\">File</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>\n            New File\n            <ui-menubar-shortcut slot=\"shortcut\">⌘N</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>\n            Open\n            <ui-menubar-shortcut slot=\"shortcut\">⌘O</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>\n            Save\n            <ui-menubar-shortcut slot=\"shortcut\">⌘S</ui-menubar-shortcut>\n          </ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"edit\">\n        <ui-menubar-trigger slot=\"trigger\">Edit</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>\n            Undo\n            <ui-menubar-shortcut slot=\"shortcut\">⌘Z</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>\n            Redo\n            <ui-menubar-shortcut slot=\"shortcut\">⌘⇧Z</ui-menubar-shortcut>\n          </ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n\nexport const Complex: Story = {\n  render: () => html`\n    <ui-menubar>\n      <ui-menubar-menu value=\"file\">\n        <ui-menubar-trigger slot=\"trigger\">File</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-group>\n            <ui-menubar-item>\n              New File\n              <ui-menubar-shortcut slot=\"shortcut\">⌘N</ui-menubar-shortcut>\n            </ui-menubar-item>\n            <ui-menubar-item>\n              New Window\n              <ui-menubar-shortcut slot=\"shortcut\">⌘⇧N</ui-menubar-shortcut>\n            </ui-menubar-item>\n          </ui-menubar-group>\n\n          <ui-menubar-separator></ui-menubar-separator>\n\n          <ui-menubar-sub>\n            <ui-menubar-sub-trigger slot=\"trigger\">\n              Open Recent\n            </ui-menubar-sub-trigger>\n            <ui-menubar-sub-content slot=\"content\">\n              <ui-menubar-item>document.txt</ui-menubar-item>\n              <ui-menubar-item>image.png</ui-menubar-item>\n              <ui-menubar-separator></ui-menubar-separator>\n              <ui-menubar-item>More...</ui-menubar-item>\n            </ui-menubar-sub-content>\n          </ui-menubar-sub>\n\n          <ui-menubar-separator></ui-menubar-separator>\n\n          <ui-menubar-item>\n            Save\n            <ui-menubar-shortcut slot=\"shortcut\">⌘S</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item disabled>\n            Save As...\n            <ui-menubar-shortcut slot=\"shortcut\">⌘⇧S</ui-menubar-shortcut>\n          </ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"edit\">\n        <ui-menubar-trigger slot=\"trigger\">Edit</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-item>\n            Undo\n            <ui-menubar-shortcut slot=\"shortcut\">⌘Z</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>\n            Redo\n            <ui-menubar-shortcut slot=\"shortcut\">⌘⇧Z</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>\n            Cut\n            <ui-menubar-shortcut slot=\"shortcut\">⌘X</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>\n            Copy\n            <ui-menubar-shortcut slot=\"shortcut\">⌘C</ui-menubar-shortcut>\n          </ui-menubar-item>\n          <ui-menubar-item>\n            Paste\n            <ui-menubar-shortcut slot=\"shortcut\">⌘V</ui-menubar-shortcut>\n          </ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"view\">\n        <ui-menubar-trigger slot=\"trigger\">View</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-label>Appearance</ui-menubar-label>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-checkbox-item checked>\n            Show Toolbar\n          </ui-menubar-checkbox-item>\n          <ui-menubar-checkbox-item> Show Sidebar </ui-menubar-checkbox-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n\n      <ui-menubar-menu value=\"profiles\">\n        <ui-menubar-trigger slot=\"trigger\">Profiles</ui-menubar-trigger>\n        <ui-menubar-content slot=\"content\">\n          <ui-menubar-radio-group value=\"andy\">\n            <ui-menubar-radio-item value=\"andy\">Andy</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"benoit\">Benoit</ui-menubar-radio-item>\n            <ui-menubar-radio-item value=\"luis\">Luis</ui-menubar-radio-item>\n          </ui-menubar-radio-group>\n          <ui-menubar-separator></ui-menubar-separator>\n          <ui-menubar-item>Edit...</ui-menubar-item>\n        </ui-menubar-content>\n      </ui-menubar-menu>\n    </ui-menubar>\n  `,\n};\n"}]}