# Menubar

A visually persistent menu common in desktop applications.

- Install: `php artisan blatui:add menubar`
- Source: https://blatui.remix-it.com/r/menubar.json
- Composer: `composer require mallardduck/blade-lucide-icons gehrisandro/tailwind-merge-laravel`
- npm: `npm install -D @alpinejs/anchor`

## resources/views/components/ui/menubar.blade.php

```blade
<div
    data-slot="menubar"
    role="menubar"
    aria-orientation="horizontal"
    x-data="blatMenubar()"
    {{ $attributes->twMerge('bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs') }}
>
    {{ $slot }}
</div>
```

## resources/views/components/ui/menubar-checkbox-item.blade.php

```blade
@props([
    'checked' => false,
    'disabled' => false,
    'closeOnSelect' => false,
])

<x-ui.menu-checkbox-item
    :data-slot="'menubar-checkbox-item'"
    :checked="$checked"
    :disabled="$disabled"
    :close-on-select="$closeOnSelect"
    {{ $attributes }}
>{{ $slot }}</x-ui.menu-checkbox-item>
```

## resources/views/components/ui/menubar-content.blade.php

```blade
@props([
    'align' => 'start',
    'side' => 'bottom',
    'sideOffset' => 8,
])

@php
    $placement = $side.($align === 'center' ? '' : '-'.$align);
    $anchorAttr = 'x-anchor.fixed.'.$placement.'.offset.'.$sideOffset.'="$refs.trigger"';
@endphp

<template x-teleport="body" wire:ignore>
    <div
        x-blat-dialog-layer
        x-show="active === id"
        x-cloak
        {!! $anchorAttr !!}
        @click.outside="if (active === id) closeMenu(false)"
        @keydown.escape.prevent.stop="closeMenu()"
        @keydown.tab.prevent.stop="closeMenu(false)"
        @keydown.left.prevent.stop="moveTrigger(-1, id)"
        @keydown.right.prevent.stop="moveTrigger(1, id)"
        @keydown="$blatNav($event); $blatType($event)"
        :id="id"
        :aria-labelledby="id + '-trigger'"
        role="menu"
        aria-orientation="vertical"
        tabindex="-1"
        data-slot="menubar-content"
        :data-state="active === id ? 'open' : 'closed'"
        x-transition:enter="transition ease-out duration-150"
        x-transition:enter-start="opacity-0 scale-95"
        x-transition:enter-end="opacity-100 scale-100"
        x-transition:leave="transition ease-in duration-100"
        x-transition:leave-start="opacity-100 scale-100"
        x-transition:leave-end="opacity-0 scale-95"
        {{ $attributes->twMerge('bg-popover text-popover-foreground fixed z-50 min-w-[12rem] origin-top-left overflow-hidden rounded-md border p-1 shadow-md outline-none') }}
    >
        {{ $slot }}
    </div>
</template>
```

## resources/views/components/ui/menubar-group.blade.php

```blade
<x-ui.menu-group :data-slot="'menubar-group'" :label-slot="'menubar-label'" {{ $attributes }}>{{ $slot }}</x-ui.menu-group>
```

## resources/views/components/ui/menubar-item.blade.php

```blade
@props([
    'href' => null,
    'variant' => 'default',
    'inset' => false,
    'disabled' => false,
])

@php
    // menubar uses a slightly different destructive-variant class set (no dark: focus
    // background, hover ordered before focus:text) and always wires @click="closeMenu()"
    // without merging/stripping any caller @click, so pass the exact string + mergeClick=false.
    $menubarClasses = "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:hover:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[inset]:pl-8";

    // menubar-item never exposed `type`/`closeOnSelect`; it always submits as a button
    // and always closes on select. Drop any caller values so the primitive can't change.
    $attributes = $attributes->except(['type', 'closeOnSelect', 'close-on-select']);
@endphp

<x-ui.menu-item
    :data-slot="'menubar-item'"
    :classes="$menubarClasses"
    :merge-click="false"
    :click-conditional="false"
    :href="$href"
    :variant="$variant"
    :inset="$inset"
    :disabled="$disabled"
    {{ $attributes }}
>{{ $slot }}</x-ui.menu-item>
```

## resources/views/components/ui/menubar-label.blade.php

```blade
@props(['inset' => false])

<x-ui.menu-label
    :data-slot="'menubar-label'"
    :inset="$inset"
    {{ $attributes }}
>{{ $slot }}</x-ui.menu-label>
```

## resources/views/components/ui/menubar-menu.blade.php

```blade
@php $menuId = 'menubar-'.\Illuminate\Support\Str::random(8); @endphp

<div data-slot="menubar-menu" x-data="{ id: @js($menuId) }" {{ $attributes->twMerge('relative') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/menubar-radio-group.blade.php

```blade
@props(['value' => ''])

<div
    data-slot="menubar-radio-group"
    role="group"
    x-data="{ radioValue: @js((string) $value) }"
    {{ $attributes }}
>
    {{ $slot }}
</div>
```

## resources/views/components/ui/menubar-radio-item.blade.php

```blade
@props([
    'value' => '',
    'closeOnSelect' => false,
])

<x-ui.menu-radio-item
    :data-slot="'menubar-radio-item'"
    :value="$value"
    :close-on-select="$closeOnSelect"
    {{ $attributes }}
>{{ $slot }}</x-ui.menu-radio-item>
```

## resources/views/components/ui/menubar-separator.blade.php

```blade
<x-ui.menu-separator :data-slot="'menubar-separator'" {{ $attributes }} />
```

## resources/views/components/ui/menubar-shortcut.blade.php

```blade
<x-ui.menu-shortcut :data-slot="'menubar-shortcut'" {{ $attributes }}>{{ $slot }}</x-ui.menu-shortcut>
```

## resources/views/components/ui/menubar-sub.blade.php

```blade
<div
    data-slot="menubar-sub"
    x-data="blatMenu()"
    x-id="['blat-submenu', 'blat-submenu-trigger']"
    @mouseenter="open = true; cancelClose()"
    @mouseleave="closeSoon()"
    {{ $attributes->twMerge('relative') }}
>
    {{ $slot }}
</div>
```

## resources/views/components/ui/menubar-sub-content.blade.php

```blade
<template x-teleport="body" wire:ignore>
    <div
        x-blat-dialog-layer
        x-show="open"
        x-cloak
        x-ref="submenu"
        x-init="_menu = $el"
        x-anchor.fixed.right-start.offset.4="$refs.subtrigger"
        @mouseenter="cancelClose()"
        @mouseleave="closeSoon()"
        @keydown.escape.prevent.stop="closeMenu()"
        @keydown.left.prevent.stop="closeMenu()"
        @keydown.stop="$blatNav($event); $blatType($event)"
        :id="$id('blat-submenu')"
        :aria-labelledby="$id('blat-submenu-trigger')"
        role="menu"
        aria-orientation="vertical"
        tabindex="-1"
        data-slot="menubar-sub-content"
        :data-state="open ? 'open' : 'closed'"
        x-transition:enter="transition ease-out duration-150"
        x-transition:enter-start="opacity-0 scale-95"
        x-transition:enter-end="opacity-100 scale-100"
        x-transition:leave="transition ease-in duration-100"
        x-transition:leave-start="opacity-100 scale-100"
        x-transition:leave-end="opacity-0 scale-95"
        {{ $attributes->twMerge('bg-popover text-popover-foreground fixed z-50 min-w-[8rem] origin-top-left overflow-hidden rounded-md border p-1 shadow-lg outline-none') }}
    >
        {{ $slot }}
    </div>
</template>
```

## resources/views/components/ui/menubar-sub-trigger.blade.php

```blade
@props(['inset' => false])

<div
    role="menuitem"
    tabindex="-1"
    x-ref="subtrigger"
    x-init="_trigger = $el"
    data-slot="menubar-sub-trigger"
    aria-haspopup="menu"
    :aria-expanded="open"
    :aria-controls="$id('blat-submenu')"
    :id="$id('blat-submenu-trigger')"
    @click="toggleMenu()"
    @keydown.right.prevent.stop="openMenu('first')"
    @keydown.enter.prevent.stop="openMenu('first')"
    @keydown.space.prevent.stop="openMenu('first')"
    @if ($inset) data-inset @endif
    :data-state="open ? 'open' : 'closed'"
    {{ $attributes->twMerge("focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4") }}
>
    {{ $slot }}
    <x-lucide-chevron-right class="ml-auto size-4" aria-hidden="true" />
</div>
```

## resources/views/components/ui/menubar-trigger.blade.php

```blade
<button
    type="button"
    x-ref="trigger"
    role="menuitem"
    aria-haspopup="menu"
    x-init="register(id, $el)"
    :id="id + '-trigger'"
    :aria-controls="id"
    :aria-expanded="active === id"
    :tabindex="rovingId === id ? 0 : -1"
    @click="toggleMenu(id)"
    @focus="rovingId = id"
    @mouseenter="if (active !== null) { active = id; rovingId = id }"
    @keydown.down.prevent.stop="openMenu(id)"
    @keydown.enter.prevent.stop="openMenu(id)"
    @keydown.space.prevent.stop="openMenu(id)"
    @keydown.right.prevent.stop="moveTrigger(1, id)"
    @keydown.left.prevent.stop="moveTrigger(-1, id)"
    :data-state="active === id ? 'open' : 'closed'"
    data-slot="menubar-trigger"
    {{ $attributes->twMerge('focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none') }}
>{{ $slot }}</button>
```
