# Dropdown

- Entry point: `@koobiq/components/dropdown`
- NgModule: `KbqDropdownModule`
- Page: https://koobiq.io/en/components/dropdown/overview

## Overview

Dropdown displays a menu with actions or options next to a trigger and supports disabled, loading, footer, and keyboard interaction states.

**Example: Dropdown** (`dropdown-overview`)

```ts dropdown-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqOptionModule } from '@koobiq/components/core';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTitleModule } from '@koobiq/components/title';

@Component({
    selector: 'dropdown-overview-example',
    imports: [
        KbqDropdownModule,
        KbqDividerModule,
        KbqButtonModule,
        KbqIconModule,
        KbqTitleModule,
        KbqOptionModule
    ],
    templateUrl: 'dropdown-overview-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownOverviewExample {}
```

```html dropdown-overview-example.html
<button kbq-button [kbqDropdownTriggerFor]="appDropdown">
    dropdown
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdown="kbqDropdown" [panelMaxWidth]="200">
    <button kbq-dropdown-item>Text Normal</button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item kbq-title>
        Text Normal and very long text Text Normal and very long text Text Normal and very long text Text Normal and
        very long text
    </button>

    <button kbq-dropdown-item progress>Loading item</button>

    <kbq-divider />

    <div kbqDropdownGroupHeader>Header</div>

    <button disabled kbq-dropdown-item>Disabled</button>

    <kbq-divider />

    <kbq-optgroup label="Subheading" />

    <button disabled kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16"></i>
        Disabled with icon
    </button>

    <kbq-divider />

    <button kbq-dropdown-item>Point 2</button>

    <button kbq-dropdown-item>Point 3</button>

    <button kbq-dropdown-item>Link</button>
</kbq-dropdown>
```

### Disabled Trigger

**Example: Dropdown disabled** (`dropdown-disabled`)

```ts dropdown-disabled-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqCheckboxModule } from '@koobiq/components/checkbox';
import { KbqOptionModule } from '@koobiq/components/core';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTitleModule } from '@koobiq/components/title';

@Component({
    selector: 'dropdown-disabled-example',
    imports: [
        KbqDropdownModule,
        FormsModule,
        KbqCheckboxModule,
        KbqDividerModule,
        KbqButtonModule,
        KbqIconModule,
        KbqTitleModule,
        KbqOptionModule
    ],
    templateUrl: 'dropdown-disabled-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownDisabledExample {
    disabled = false;
}
```

```html dropdown-disabled-example.html
<kbq-checkbox class="layout-margin-right-l" [(ngModel)]="disabled">toggle disabled state</kbq-checkbox>

<button kbq-button [disabled]="disabled" [kbqDropdownTriggerFor]="appDropdown">
    dropdown
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdown="kbqDropdown" [panelMaxWidth]="200">
    <button kbq-dropdown-item>Text Normal</button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item kbq-title>
        Text Normal and very long text Text Normal and very long text Text Normal and very long text Text Normal and
        very long text
    </button>

    <kbq-divider />

    <div kbqDropdownGroupHeader>Header</div>

    <button disabled kbq-dropdown-item>Disabled</button>

    <kbq-divider />

    <kbq-optgroup label="Subheading" />

    <button disabled kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16"></i>
        Disabled with icon
    </button>

    <kbq-divider />

    <button kbq-dropdown-item>Point 2</button>

    <button kbq-dropdown-item>Point 3</button>

    <button kbq-dropdown-item>Link</button>
</kbq-dropdown>
```

### Progress state

Add the `progress` attribute to `kbq-dropdown-item` to show a loading shimmer on the item. It can be combined with `disabled` to also prevent interaction while loading.

**Example: Dropdown item progress** (`dropdown-item-progress`)

```ts dropdown-item-progress-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';

@Component({
    selector: 'dropdown-item-progress-example',
    imports: [KbqDropdownModule, KbqButtonModule, KbqIconModule, KbqProgressSpinnerModule],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="appDropdown">
            Trigger
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #appDropdown="kbqDropdown">
            <button kbq-dropdown-item>Menu item 1</button>
            <button kbq-dropdown-item>Menu item 2</button>
            <button kbq-dropdown-item>Menu item 3</button>
            <button kbq-dropdown-item progress [kbqDropdownTriggerFor]="loadingSubmenu">Menu item 4</button>
            <button kbq-dropdown-item>Menu item 5</button>
            <button kbq-dropdown-item>Menu item 6</button>
            <button kbq-dropdown-item progress disabled>Menu item 7</button>
            <button kbq-dropdown-item>Menu item 8</button>
        </kbq-dropdown>

        <kbq-dropdown #loadingSubmenu="kbqDropdown">
            <div class="layout-row layout-align-center-center" [style.height.px]="100">
                <kbq-progress-spinner mode="indeterminate" />
            </div>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-align-center-center',
        '[style.height.px]': '200'
    }
})
export class DropdownItemProgressExample {}
```

### Secondary action

You can add an extra button inside a menu item (for example, to go to settings). It works independently: clicking it doesn't close the menu, and you can reach it with the Tab key.

The menu item itself should be a regular block, not a button.

If the item is in a loading state, the extra button stays visible but becomes unavailable.

**Example: Dropdown item with a secondary action** (`dropdown-item-action`)

```ts dropdown-item-action-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTitleDirective } from '@koobiq/components/title';

@Component({
    selector: 'dropdown-item-action-example',
    imports: [KbqDropdownModule, KbqButtonModule, KbqIconModule, KbqTitleDirective],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="checkDropdown">
            Check
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #checkDropdown="kbqDropdown">
            <div kbq-dropdown-item kbq-title>
                Check with rules
                <a
                    kbq-icon-button="kbq-gear_16"
                    kbqDropdownItemAction
                    size="compact"
                    aria-label="Rules settings"
                    target="_blank"
                    href="en/components/button"
                    [color]="'contrast-fade'"
                ></a>
            </div>

            <div kbq-dropdown-item>
                Check by hash sum
                <a
                    kbq-icon-button="kbq-gear_16"
                    kbqDropdownItemAction
                    size="compact"
                    aria-label="Hash sum settings"
                    target="_blank"
                    href="en/components/icon"
                    [color]="'contrast-fade'"
                ></a>
            </div>

            <div kbq-dropdown-item disabled>
                Not available
                <a
                    kbq-icon-button="kbq-gear_16"
                    kbqDropdownItemAction
                    size="compact"
                    aria-label="Hash sum settings (unavailable)"
                    target="_blank"
                    href="en/components/icon"
                    [color]="'contrast-fade'"
                    [disabled]="true"
                ></a>
            </div>

            <div kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">
                Nested
                <a
                    kbq-icon-button="kbq-gear_16"
                    kbqDropdownItemAction
                    size="compact"
                    aria-label="More actions"
                    target="_blank"
                    href="en/components/tag"
                    [color]="'contrast-fade'"
                ></a>
            </div>

            <kbq-dropdown #appDropdownNested="kbqDropdown">
                <button kbq-dropdown-item>Edit</button>
                <button kbq-dropdown-item>Delete</button>
            </kbq-dropdown>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-align-center-center',
        '[style.height.px]': '200'
    }
})
export class DropdownItemActionExample {}
```

### Item as a link

A menu item can be made into a regular link — then the entire block will navigate to another page.

**Example: Dropdown item as a link** (`dropdown-item-link`)

```ts dropdown-item-link-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'dropdown-item-link-example',
    imports: [KbqDropdownModule, KbqButtonModule, KbqIconModule],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="actionsDropdown">
            Actions
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #actionsDropdown="kbqDropdown">
            <a kbq-dropdown-item href="https://angular.dev/" target="_blank" rel="noopener noreferrer">Angular</a>
            <a kbq-dropdown-item href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">
                TypeScript
            </a>
            <a kbq-dropdown-item href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer">
                Storybook
            </a>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-align-center-center',
        '[style.height.px]': '200'
    }
})
export class DropdownItemLinkExample {}
```

### Footer

You can place auxiliary elements in the footer: [buttons](https://koobiq.io/en/components/button.md), [links](https://koobiq.io/en/components/link.md), hints.

**Example: Dropdown with footer** (`dropdown-with-footer`)

```ts dropdown-with-footer-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLink } from '@koobiq/components/link';

@Component({
    selector: 'dropdown-with-footer-example',
    imports: [KbqDropdownModule, KbqButtonModule, KbqLink, KbqIconModule],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="appDropdown">
            Trigger
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #appDropdown="kbqDropdown">
            <button kbq-dropdown-item>Menu item 1</button>
            <button kbq-dropdown-item>Menu item 2</button>
            <button kbq-dropdown-item>Menu item 3</button>
            <button kbq-dropdown-item [kbqDropdownTriggerFor]="submenu">Menu item 4</button>
            <button kbq-dropdown-item>Menu item 5</button>

            <kbq-dropdown-footer>
                <a
                    href="https://github.com/koobiq/angular-components"
                    target="_blank"
                    rel="noopener noreferrer"
                    kbqLink
                    external
                >
                    GitHub
                </a>
            </kbq-dropdown-footer>
        </kbq-dropdown>

        <kbq-dropdown #submenu="kbqDropdown">
            <button kbq-dropdown-item>Menu item 1</button>
            <button kbq-dropdown-item>Menu item 2</button>
            <button kbq-dropdown-item>Menu item 3</button>
            <kbq-dropdown-footer class="example-submenu-footer">
                Footer text is multiline and wraps across lines
            </kbq-dropdown-footer>
        </kbq-dropdown>
    `,
    styles: `
        :host {
            height: 200px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .example-submenu-footer {
            font-size: var(--kbq-typography-text-compact-font-size);
            color: var(--kbq-foreground-contrast-secondary);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownWithFooterExample {}
```

### Open by Arrow Down ↓

**Example: Dropdown open by arrow down** (`dropdown-open-by-arrow-down`)

```ts dropdown-open-by-arrow-down-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqCheckboxModule } from '@koobiq/components/checkbox';
import { KbqOptionModule } from '@koobiq/components/core';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTitleModule } from '@koobiq/components/title';

@Component({
    selector: 'dropdown-open-by-arrow-down-example',
    imports: [
        KbqDropdownModule,
        FormsModule,
        KbqCheckboxModule,
        KbqDividerModule,
        KbqButtonModule,
        KbqIconModule,
        KbqTitleModule,
        KbqInputModule,
        KbqOptionModule
    ],
    templateUrl: 'dropdown-open-by-arrow-down-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownOpenByArrowDownExample {
    openByArrowDown = true;
}
```

```html dropdown-open-by-arrow-down-example.html
<kbq-checkbox class="layout-margin-right-l" [(ngModel)]="openByArrowDown">toggle openByArrowDown</kbq-checkbox>

<button kbq-button [kbqDropdownTriggerFor]="appDropdown" [openByArrowDown]="openByArrowDown">
    dropdown
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdown="kbqDropdown" [panelMaxWidth]="200">
    <button kbq-dropdown-item>Text Normal</button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item kbq-title>
        Text Normal and very long text Text Normal and very long text Text Normal and very long text Text Normal and
        very long text
    </button>

    <kbq-divider />

    <div kbqDropdownGroupHeader>Header</div>

    <button disabled kbq-dropdown-item>Disabled</button>

    <kbq-divider />

    <kbq-optgroup label="Subheading" />

    <button disabled kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16"></i>
        Disabled with icon
    </button>

    <kbq-divider />
    <button kbq-dropdown-item>Point 2</button>
</kbq-dropdown>
```

### Lazy Load Data

**Example: Dropdown lazy load data** (`dropdown-lazyload-data`)

```ts dropdown-lazyload-data-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';

@Component({
    selector: 'dropdown-lazyload-data-example',
    imports: [
        KbqDropdownModule,
        FormsModule,
        KbqButtonModule,
        KbqIconModule,
        KbqInputModule
    ],
    templateUrl: 'dropdown-lazyload-data-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownLazyloadDataExample {
    someValue = 'Lazy Value';
}
```

```html dropdown-lazyload-data-example.html
<kbq-form-field class="layout-margin-bottom-l">
    <input autocomplete="off" kbqInput style="max-width: 200px" type="text" [(ngModel)]="someValue" />
</kbq-form-field>

<button kbq-button [kbqDropdownTriggerData]="{ someValue: someValue }" [kbqDropdownTriggerFor]="appDropdownLazy">
    dropdown lazy
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdownLazy="kbqDropdown">
    <ng-template let-someValue="someValue" kbqDropdownContent>
        <button kbq-dropdown-item>dropdown</button>
        <button kbq-dropdown-item>
            Point 1
            <span kbqDropdownItemCaption>Some info</span>
        </button>
        <button kbq-dropdown-item>Value: {{ someValue }}</button>
    </ng-template>
</kbq-dropdown>
```

### Nested Elements

**Example: Dropdown nested** (`dropdown-nested`)

```ts dropdown-nested-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'dropdown-nested-example',
    imports: [
        KbqDropdownModule,
        KbqButtonModule,
        KbqIconModule,
        KbqDividerModule
    ],
    templateUrl: 'dropdown-nested-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownNestedExample {}
```

```html dropdown-nested-example.html
<button kbq-button [kbqDropdownTriggerFor]="appDropdownWithNested">
    dropdown
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdownWithNested="kbqDropdown">
    <button kbq-dropdown-item>Search settings</button>

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Text</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Paragraph styles</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Align & intent</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Line spacing</button>
    <button kbq-dropdown-item [disabled]="true" [kbqDropdownTriggerFor]="appDropdownNested">Columns</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Bullets & numbering</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Headers & footers</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Page numbers</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Table</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Image</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Clear formatting</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Border & lines</button>
</kbq-dropdown>

<kbq-dropdown #appDropdownNested="kbqDropdown">
    <button kbq-dropdown-item>Border and shading</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Normal text</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Title</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Subtitle</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 1</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 2</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 3</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 4</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 5</button>
    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Heading 6</button>

    <kbq-divider />

    <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNestedNested">Options</button>
</kbq-dropdown>

<kbq-dropdown #appDropdownNestedNested="kbqDropdown">
    <button kbq-dropdown-item>Apply 'Title'</button>
    <button kbq-dropdown-item>Update 'Title' to match style</button>
</kbq-dropdown>
```

### Safe Area

This mechanism prevents an open nested submenu from closing prematurely while the pointer is moving.
The submenu stays open even if the pointer touches sibling items along the way, as long as the
movement stays within the designated area. It can be configured in two ways:

- locally — on the panel that contains the nested triggers, through its `safeArea` property;
- globally — through `KBQ_DROPDOWN_DEFAULT_OPTIONS`.

**Example: Dropdown safe area** (`dropdown-safe-area`)

```ts dropdown-safe-area-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqToggleModule } from '@koobiq/components/toggle';

@Component({
    selector: 'dropdown-safe-area-example',
    imports: [KbqDropdownModule, KbqToggleModule, KbqButtonModule, KbqIconModule, KbqDividerModule],
    template: `
        <kbq-toggle [checked]="safeArea()" (change)="safeArea.set($event.checked)">Safe Area</kbq-toggle>

        <button kbq-button [kbqDropdownTriggerFor]="appDropdown">
            File
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #appDropdown="kbqDropdown" [safeArea]="safeArea()">
            <button kbq-dropdown-item>New</button>
            <button kbq-dropdown-item>Open</button>
            <button kbq-dropdown-item [kbqDropdownTriggerFor]="appDropdownNested">Share</button>
            <button kbq-dropdown-item>Save</button>
            <button kbq-dropdown-item>Save as...</button>
            <button kbq-dropdown-item>Rename</button>
            <button kbq-dropdown-item>Move</button>

            <kbq-divider />

            <button kbq-dropdown-item>Print</button>
        </kbq-dropdown>

        <kbq-dropdown #appDropdownNested="kbqDropdown">
            <button kbq-dropdown-item>Email as attachment</button>
            <button kbq-dropdown-item>Publish to web</button>
            <button kbq-dropdown-item>Share with people</button>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-align-center-center layout-gap-s'
    }
})
export class DropdownSafeAreaExample {
    /**
     * Enabled by default so the effect is visible immediately — move the pointer diagonally from
     * "Share" toward its submenu, crossing "Save" on the way, and the submenu stays open. Toggle off
     * to compare against the default (no safe area) behavior.
     */
    readonly safeArea = signal(true);
}
```

### Navigation Wrap

A "cyclic navigation" mode where reaching one end of the list loops back to the other end.

**Example: Dropdown navigation wrap** (`dropdown-navigation-wrap`)

```ts dropdown-navigation-wrap-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqOptionModule } from '@koobiq/components/core';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'dropdown-navigation-wrap-example',
    imports: [
        KbqDropdownModule,
        KbqButtonModule,
        KbqIconModule,
        KbqDividerModule,
        KbqOptionModule
    ],
    templateUrl: 'dropdown-navigation-wrap-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownNavigationWrapExample {}
```

```html dropdown-navigation-wrap-example.html
<button kbq-button [kbqDropdownTriggerFor]="appDropdown">
    dropdown
    <i kbq-icon="kbq-chevron-down-s_16"></i>
</button>

<kbq-dropdown #appDropdown="kbqDropdown" [panelMaxWidth]="200" [navigationWithWrap]="true">
    <button kbq-dropdown-item>Text Normal</button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
        Text Normal
        <div kbqDropdownItemCaption>Caption</div>
    </button>

    <button kbq-dropdown-item>
        <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
        Text Normal
    </button>

    <button kbq-dropdown-item kbq-title>
        Text Normal and very long text Text Normal and very long text Text Normal and very long text Text Normal and
        very long text
    </button>

    <kbq-divider />

    <div kbqDropdownGroupHeader>Header</div>

    <button disabled kbq-dropdown-item>Disabled</button>

    <kbq-divider />

    <kbq-optgroup label="Subheading" />

    <button disabled kbq-dropdown-item>Disabled</button>

    <kbq-divider />

    <button kbq-dropdown-item>Point 2</button>

    <button kbq-dropdown-item>Point 3</button>

    <button kbq-dropdown-item>Link</button>
</kbq-dropdown>
```

### Dynamic Rendering

Recursive rendering of an array or object, allowing the creation of nested dropdown menus.

**Example: Dropdown Recursive Template** (`dropdown-recursive-template`)

```ts dropdown-recursive-template-example.ts
import {
    ChangeDetectionStrategy,
    Component,
    Directive,
    Injector,
    OnInit,
    TemplateRef,
    ViewContainerRef,
    inject,
    input
} from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KBQ_DROPDOWN_PANEL, KbqDropdown, KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';

type DropdownItem = {
    label: string;
    children?: DropdownItem[];
};

@Directive({
    selector: '[exampleDropdownOutlet]'
})
export class ExampleDropdownPortal implements OnInit {
    private viewContainerRef = inject(ViewContainerRef);

    readonly context = input({}, { alias: 'exampleDropdownOutletContext' });
    readonly template = input<TemplateRef<any>>(undefined!, { alias: 'exampleDropdownOutlet' });

    ngOnInit() {
        this.viewContainerRef.createEmbeddedView(this.template(), this.context(), {
            injector: Injector.create({
                parent: this.viewContainerRef.injector,
                // Provides a custom dependency:
                // - It binds `KBQ_DROPDOWN_PANEL` to the existing `KbqDropdown` instance,
                //   ensuring that any injected `KBQ_DROPDOWN_PANEL` token resolves to closest `KbqDropdown`.
                providers: [
                    { provide: KBQ_DROPDOWN_PANEL, useExisting: KbqDropdown }
                ]
            })
        });
    }
}

@Component({
    selector: 'dropdown-recursive-template-example',
    imports: [
        KbqDropdownModule,
        KbqButtonModule,
        KbqIconModule,
        ExampleDropdownPortal
    ],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="egDropdown">
            dropdown
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #egDropdown="kbqDropdown">
            @for (item of items; track item) {
                <ng-container
                    [exampleDropdownOutlet]="dropdownItemTpl"
                    [exampleDropdownOutletContext]="{ item: item }"
                />
            }

            <ng-template #dropdownItemTpl let-item="item">
                @if (item.children) {
                    <button kbq-dropdown-item [kbqDropdownTriggerFor]="egDropdownNested">
                        {{ item.label }}
                    </button>

                    <kbq-dropdown #egDropdownNested="kbqDropdown">
                        @for (child of item.children; track child) {
                            <ng-container
                                [exampleDropdownOutlet]="dropdownItemTpl"
                                [exampleDropdownOutletContext]="{ item: child }"
                            />
                        }
                    </kbq-dropdown>
                } @else {
                    <button kbq-dropdown-item>{{ item.label }}</button>
                }
            </ng-template>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownRecursiveTemplateExample {
    items: DropdownItem[] = [
        { label: 'Level 1 - Security Settings' },
        {
            label: 'Level 1 - Encryption',
            children: [
                { label: 'Level 2 - SSL/TLS' },
                {
                    label: 'Level 2 - AES Encryption',
                    children: [
                        {
                            label: 'Level 3 - Key Management',
                            children: [
                                { label: 'Level 4 - Public Key Infrastructure (PKI)' },
                                { label: 'Level 4 - Symmetric Key Management' },
                                { label: 'Level 4 - Key Exchange Algorithms' }
                            ]
                        },
                        { label: 'Level 3 - Encryption Standards' }
                    ]
                },
                { label: 'Level 2 - Digital Signatures' }
            ]
        },
        {
            label: 'Level 1 - Access Control',
            children: [
                { label: 'Level 2 - Role-Based Access Control (RBAC)' },
                { label: 'Level 2 - Multi-Factor Authentication (MFA)' },
                { label: 'Level 2 - Least Privilege Principle' }
            ]
        }
    ];
}
```

### Menu Search

The `kbqDropdownSearch` directive turns a `kbq-form-field` into the panel's search field: it styles the field for the panel and takes over the keyboard.

The caret stays in the field while the menu is open: `ArrowUp` and `ArrowDown` move the highlight across the items, `Enter` picks the highlighted one, the first `Escape` clears the query and the second one closes the menu.

Search splits a multi-word query into parts and searches for them independently, trims leading and trailing spaces, is case-insensitive, and folds diacritics. The algorithm is described in the [Smart search guide](https://koobiq.io/en/other/search-smart.md).

**Example: Dropdown with filter** (`dropdown-with-filter`)

```ts dropdown-with-filter-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import {
    createSearchPredicate,
    KbqHighlightBackgroundPipe,
    KbqOptgroup,
    tokenizeSearchQuery
} from '@koobiq/components/core';
import { KbqDivider } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqCleaner, KbqFormField, KbqPrefix } from '@koobiq/components/form-field';
import { KbqIcon } from '@koobiq/components/icon';
import { KbqInput } from '@koobiq/components/input';
import { debounceTime, distinctUntilChanged, map } from 'rxjs';

@Component({
    selector: 'dropdown-with-filter-example',
    imports: [
        KbqDropdownModule,
        KbqButtonModule,
        KbqDivider,
        KbqOptgroup,
        KbqFormField,
        KbqPrefix,
        KbqIcon,
        KbqCleaner,
        KbqInput,
        ReactiveFormsModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <button kbq-button [kbqDropdownTriggerFor]="dropdown" (dropdownClosed)="handleClosed()">
            Trigger
            <i kbq-icon="kbq-chevron-down-s_16"></i>
        </button>

        <kbq-dropdown #dropdown="kbqDropdown">
            <kbq-form-field kbqDropdownSearch>
                <i kbqPrefix kbq-icon="kbq-magnifying-glass_16"></i>
                <input autocomplete="off" kbqInput placeholder="Search" [formControl]="control" />
                <kbq-cleaner />
            </kbq-form-field>

            <kbq-divider />

            @let filtered = filteredGroups();

            @for (item of filtered[0]; track item) {
                <button kbq-dropdown-item>
                    <span [innerHTML]="item | kbqHighlightBackground: searchTokens"></span>
                </button>
            }

            @if (filtered[0].length > 0 && filtered[1].length > 0) {
                <kbq-divider />
            }

            @if (filtered[1].length > 0) {
                <kbq-optgroup label="Subheader for actions" />
            }

            @for (item of filtered[1]; track item) {
                <button kbq-dropdown-item>
                    <span [innerHTML]="item | kbqHighlightBackground: searchTokens"></span>
                </button>
            }

            @if (filtered[0].length === 0 && filtered[1].length === 0) {
                <button kbq-dropdown-item disabled>Nothing found</button>
            }
        </kbq-dropdown>
    `,
    styles: `
        :host {
            height: 200px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownWithFilterExample {
    private readonly groups = [
        ['New tab', 'New window'],
        ['Action one', 'Action two', 'Action three']
    ];
    protected readonly control = new FormControl('');
    protected readonly filteredGroups = toSignal(
        this.control.valueChanges.pipe(
            debounceTime(300),
            distinctUntilChanged(),
            map((value) => {
                const predicate = createSearchPredicate(value ?? '');

                return this.groups.map((group) => group.filter(predicate));
            })
        ),
        { initialValue: this.groups }
    );

    protected get searchTokens(): string[] {
        return tokenizeSearchQuery(this.control.value ?? '');
    }

    protected handleClosed(): void {
        this.control.reset();
    }
}
```

### Menu at the caret

`kbqDropdownTriggerOrigin` detaches the panel from the trigger element: bind an element, or a rectangle in viewport coordinates, and the panel is positioned against that. `kbqCreateCaretOrigin` from `@koobiq/components/core` builds such a rectangle out of a measurement it repeats on every reposition — here the `/` and the query typed after it, which `kbqGetTextQuery` finds and `kbqListenForCaretMoves` keeps watching — so the panel keeps up with the caret. `updatePosition()` moves a panel that is already open, and `null` anchors it back to the trigger.

Keep the trigger off the editor: it answers `Enter` and `Space` itself, so it belongs on a separate element. `[kbqDropdownTriggerAutoFocus]="false"` leaves the caret in the text, and `[activeDescendantNavigation]="true"` on the panel makes the arrow keys move the highlight without taking focus — the editor then hands `ArrowUp`, `ArrowDown`, `Enter` and `Escape` over with `handleKeydown($event)`. The highlighted item is not announced yet: the panel sets no `aria-activedescendant`.

**Example: Dropdown at the caret** (`dropdown-slash-menu`)

```ts dropdown-slash-menu-example.ts
import { DOCUMENT } from '@angular/common';
import {
    afterNextRender,
    ChangeDetectionStrategy,
    ChangeDetectorRef,
    Component,
    computed,
    DestroyRef,
    ElementRef,
    inject,
    Renderer2,
    signal,
    viewChild
} from '@angular/core';
import {
    createSearchPredicate,
    KbqCaretRect,
    kbqCreateCaretOrigin,
    kbqGetSelectionRect,
    kbqGetTextQuery,
    KbqHighlightBackgroundPipe,
    kbqListenForCaretMoves,
    KbqOptgroup,
    tokenizeSearchQuery
} from '@koobiq/components/core';
import { KbqDropdown, KbqDropdownModule, KbqDropdownTrigger } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';

/** `Node.TEXT_NODE`, spelled out so that the example never reads a DOM global. */
const TEXT_NODE = 3;

const TRIGGER = '/';

const EMPTY_RECT: KbqCaretRect = { x: 0, y: 0, width: 0, height: 0 };

/** The keys the menu lives on while the caret stays in the editor. */
const MENU_KEYS = ['ArrowUp', 'ArrowDown', 'Enter', 'Escape'];

const GROUPS: { label: string; items: { name: string; icon: string }[] }[] = [
    {
        label: 'Suggested',
        items: [
            { name: 'Text', icon: 'kbq-paragraph-sign_16' },
            { name: 'Bulleted list', icon: 'kbq-list-ul_16' },
            { name: 'To-do list', icon: 'kbq-checkbox-multiple_16' }
        ]
    },
    {
        label: 'Basic blocks',
        items: [
            { name: 'Numbered list', icon: 'kbq-list-ol_16' },
            { name: 'Code', icon: 'kbq-code_16' },
            { name: 'Table', icon: 'kbq-table_16' },
            { name: 'Image', icon: 'kbq-image_16' },
            { name: 'Divider', icon: 'kbq-minus_16' }
        ]
    }
];

@Component({
    selector: 'dropdown-slash-menu-example',
    imports: [
        KbqDropdownModule,
        KbqHighlightBackgroundPipe,
        KbqIconModule,
        KbqOptgroup
    ],
    template: `
        <div
            #editor
            aria-label="Article text"
            aria-multiline="true"
            class="example-editor kbq-text-normal"
            contenteditable="plaintext-only"
            role="textbox"
            (keydown)="handleKeydown($event)"
        >
            Type / anywhere in this text to open the block menu.
        </div>

        <!--
            The panel is positioned against the caret, so the trigger only has to stay out of the editor:
            on the editor itself it would swallow Enter and Space.
        -->
        <span
            class="example-anchor"
            [kbqDropdownTriggerAutoFocus]="false"
            [kbqDropdownTriggerFor]="menu"
            [kbqDropdownTriggerOrigin]="origin"
            [kbqDropdownTriggerRestoreFocus]="false"
        ></span>

        <kbq-dropdown
            #menu="kbqDropdown"
            class="example-slash-menu"
            [activeDescendantNavigation]="true"
            [panelWidth]="280"
        >
            @for (group of filteredGroups(); track group.label) {
                @if (group.items.length) {
                    <kbq-optgroup [label]="group.label" />

                    @for (item of group.items; track item.name) {
                        <!-- Taking focus would drop the caret the menu is anchored to. -->
                        <button kbq-dropdown-item (click)="insert(item.name)" (mousedown)="$event.preventDefault()">
                            <i [kbq-icon]="item.icon"></i>
                            <span [innerHTML]="item.name | kbqHighlightBackground: tokens()"></span>
                        </button>
                    }
                }
            }

            @if (isEmpty()) {
                <button disabled kbq-dropdown-item>Nothing found</button>
            }

            <kbq-dropdown-footer class="example-slash-menu__footer">
                <span>Close menu</span>
                <span class="example-slash-menu__hotkey">Esc</span>
            </kbq-dropdown-footer>
        </kbq-dropdown>
    `,
    styles: `
        :host {
            display: block;
            padding: 16px 0 160px;
        }

        /* The field look, taken from the form-field tokens: a contenteditable cannot live in kbq-form-field. */
        .example-editor {
            max-width: 440px;
            min-height: 96px;
            margin: 0 auto;
            padding: var(--kbq-size-xs) var(--kbq-size-s);
            border: var(--kbq-size-border-width) solid var(--kbq-line-contrast-fade);
            border-radius: var(--kbq-size-border-radius);
            outline: none;
            background: var(--kbq-background-bg);
            color: var(--kbq-foreground-contrast);
        }

        .example-editor:focus {
            border-color: var(--kbq-states-line-focus-theme);
            box-shadow: inset 0 0 0.1px var(--kbq-size-border-width) var(--kbq-states-line-focus-theme);
        }

        .example-anchor {
            display: block;
            width: 0;
            height: 0;
        }

        /* The panel lives in an overlay, outside this component's DOM. */
        ::ng-deep .example-slash-menu .kbq-dropdown__content {
            max-height: 240px;
            overflow-y: auto;
        }

        .example-slash-menu__footer {
            justify-content: space-between;
        }

        .example-slash-menu__hotkey {
            color: var(--kbq-foreground-contrast-secondary);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownSlashMenuExample {
    private readonly document = inject(DOCUMENT);
    private readonly renderer = inject(Renderer2);
    private readonly changeDetectorRef = inject(ChangeDetectorRef);
    private readonly editor = viewChild<ElementRef<HTMLElement>>('editor');
    private readonly menu = viewChild(KbqDropdown);
    private readonly trigger = viewChild(KbqDropdownTrigger);

    /** The `/…` the menu filters by, as a live range, so the panel keeps up with the token as it grows. */
    private token: Range | null = null;
    /** Whether the menu was dismissed for the token the caret still sits in. */
    private dismissed = false;

    /** Stays on the token: the rectangle is measured again every time the panel is positioned. */
    protected readonly origin = kbqCreateCaretOrigin(() => this.measureToken());
    protected readonly query = signal('');
    protected readonly tokens = computed(() => tokenizeSearchQuery(this.query()));

    protected readonly filteredGroups = computed(() => {
        const matches = createSearchPredicate(this.query());

        return GROUPS.map(({ label, items }) => ({ label, items: items.filter(({ name }) => matches(name)) }));
    });

    protected readonly isEmpty = computed(() => this.filteredGroups().every(({ items }) => !items.length));

    constructor() {
        const destroyRef = inject(DestroyRef);

        afterNextRender(() => {
            const editor = this.editor()?.nativeElement;

            if (!editor) return;

            destroyRef.onDestroy(kbqListenForCaretMoves(this.renderer, editor, () => this.syncMenu()));
        });
    }

    protected handleKeydown(event: KeyboardEvent): void {
        if (!this.trigger()?.opened || !MENU_KEYS.includes(event.key)) return;

        if (event.key === 'Escape') {
            this.dismissed = true;
        }

        // The panel never takes focus, so the editor has to hand the keys it lives on over to it.
        this.menu()?.handleKeydown(event);
    }

    protected insert(name: string): void {
        const token = this.token;
        const editor = this.editor()?.nativeElement;

        if (!token || !editor) return;

        token.deleteContents();
        token.insertNode(this.document.createTextNode(`${name} `));
        token.collapse(false);

        // A click leaves the caret behind; put it back after the inserted block.
        editor.focus();

        const selection = this.document.getSelection();

        selection?.removeAllRanges();
        selection?.addRange(token);

        this.syncMenu();
    }

    private syncMenu(): void {
        const trigger = this.trigger();

        if (!trigger) return;

        const caret = this.readCaret();
        const query = caret && kbqGetTextQuery(caret.value, caret.caret, { triggers: [TRIGGER] });
        const token = query && this.getTokenRange(query.end - query.start);

        if (!query || !token) {
            this.token = null;
            this.dismissed = false;
            trigger.close();

            return;
        }

        if (this.dismissed) return;

        this.token = token;
        this.query.set(query.text);

        // The trigger has to see the new origin, and the panel its filtered items, before it opens.
        this.changeDetectorRef.detectChanges();

        if (trigger.opened) {
            trigger.updatePosition();
        } else {
            trigger.open();
        }
    }

    private measureToken(): KbqCaretRect {
        const rect = this.token?.getBoundingClientRect();

        if (rect?.height) return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };

        const editor = this.editor()?.nativeElement;

        return (editor && kbqGetSelectionRect(editor)) ?? EMPTY_RECT;
    }

    /** The editor's text and the caret offset within it, or `null` when the caret is somewhere else. */
    private readCaret(): { value: string; caret: number } | null {
        const editor = this.editor()?.nativeElement;
        const selection = this.document.getSelection();
        const node = selection?.focusNode;

        if (!editor || !node || !selection.isCollapsed || !editor.contains(node)) return null;

        const beforeCaret = this.document.createRange();

        beforeCaret.selectNodeContents(editor);
        beforeCaret.setEnd(node, selection.focusOffset);

        return { value: editor.textContent ?? '', caret: beforeCaret.toString().length };
    }

    /** The last `length` characters before the caret, as a range that outlives the keystrokes extending it. */
    private getTokenRange(length: number): Range | null {
        const selection = this.document.getSelection();
        const node = selection?.focusNode;

        // A token is typed into one text node; anything else is left to the next keystroke.
        if (!node || node.nodeType !== TEXT_NODE || selection.focusOffset < length) return null;

        const range = this.document.createRange();

        range.setStart(node, selection.focusOffset - length);
        range.setEnd(node, selection.focusOffset);

        return range;
    }
}
```

### Horizontal alignment

Use the `xPosition` property to control horizontal alignment of the dropdown panel relative to the trigger. Supported values: `'after'` (default), `'before'`, and `'center'`.

**Example: Dropdown xPosition** (`dropdown-x-position`)

```ts dropdown-x-position-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqDropdownModule, KbqDropdownPositionX } from '@koobiq/components/dropdown';

@Component({
    selector: 'dropdown-x-position-example',
    imports: [KbqDropdownModule, KbqButtonModule, KbqDividerModule],
    template: `
        <div class="layout-row layout-gap-s">
            @for (pos of positions; track pos) {
                <button kbq-button (click)="setPosition(pos)">{{ pos }}</button>
            }
        </div>

        <button kbq-button [kbqDropdownTriggerFor]="appDropdown">Dropdown ({{ xPosition() }})</button>

        <kbq-dropdown #appDropdown="kbqDropdown" [xPosition]="xPosition()">
            <button kbq-dropdown-item>Dropdown option 1</button>
            <button kbq-dropdown-item>Dropdown option 2</button>
            <kbq-divider />
            <button kbq-dropdown-item>Dropdown option 3</button>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-align-center-center layout-gap-s'
    }
})
export class DropdownXPositionExample {
    readonly positions: KbqDropdownPositionX[] = ['before', 'after', 'center'];
    readonly xPosition = signal<KbqDropdownPositionX>('center');

    setPosition(pos: KbqDropdownPositionX) {
        this.xPosition.set(pos);
    }
}
```

### Panel width

By default the panel grows with its content, is never narrower than its trigger or than 200px, and stops at 640px. This is configured with the same `panelWidth`, `panelMinWidth` and `panelMaxWidth` attributes as [select](https://koobiq.io/en/components/select.md) and [autocomplete](https://koobiq.io/en/components/autocomplete.md):

```html
<!-- Match the trigger exactly -->
<kbq-dropdown panelWidth="auto" />

<!-- An exact width; panelMinWidth is not applied -->
<kbq-dropdown [panelWidth]="400" />

<!-- Let the panel grow further with its content -->
<kbq-dropdown [panelMaxWidth]="800" />
```

The 640px cap is soft: it limits growth by content only, so a trigger wider than 640px still gets a panel as wide as itself, and an explicit `panelWidth` is never clamped.

The defaults come from design tokens, which can be overridden by a theme:

| Token                                     | Default                                  |
| ----------------------------------------- | ---------------------------------------- |
| `--kbq-dropdown-size-container-width-min` | `200px`                                  |
| `--kbq-dropdown-size-container-width-max` | `var(--kbq-panel-size-width-max, 640px)` |

`--kbq-panel-size-width-max` is shared by every dropdown panel in the library, so setting it once on `:root` caps select, tree-select, autocomplete and dropdown together.

Nested panels and panels that overlap the trigger vertically are not matched to the trigger — they follow the tokens only.

When the trigger is only a part of a larger control, set `KbqDropdownTrigger.widthOrigin` to the element the panel should match instead. [Split button](https://koobiq.io/en/components/split-button.md) does this through its `panelAutoWidth` input.

### Layering

By default, the menu appears above the horizontal [Navbar](https://koobiq.io/en/components/navbar.md) and [Topbar](https://koobiq.io/en/components/top-bar.md). To have a menu opened from the page content slide under them while the page scrolls, mark the content area with `kbqOverlayLayer` — see [Panels under a sticky header](https://koobiq.io/en/components/core.md#panels-under-a-sticky-header). A menu opened from the bar itself, like **Actions** in the example, stays above it.

**Example: Dropdown scrolling and layering** (`dropdown-scrolling-and-layering`)

```ts dropdown-scrolling-and-layering-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
    selector: 'dropdown-scrolling-and-layering-example',
    template: `
        <iframe
            src="/examples/dropdown-scrolling-and-layering-page"
            loading="lazy"
            width="648"
            height="400"
            title="dropdown-scrolling-and-layering-example"
            style="border: none"
        ></iframe>
    `,
    styles: `
        :host {
            display: flex;
            margin: -20px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownScrollingAndLayeringExample {}
```

## API

### Components

#### KbqDropdown (component)

```ts
@Component({ selector: 'kbq-dropdown', exportAs: 'kbqDropdown' })
class KbqDropdown implements KbqDropdownPanel {
    readonly activeDescendantNavigation = input<boolean>(false);
    readonly navigationWithWrap = input<boolean>(false);
    readonly xPosition = model<KbqDropdownPositionX>(
        this.defaultOptions.xPosition
    );
    readonly yPosition = model<KbqDropdownPositionY>(
        this.defaultOptions.yPosition
    );
    readonly overlapTriggerY = model<boolean>(
        this.defaultOptions.overlapTriggerY
    );
    readonly overlapTriggerX = model<boolean>(
        this.defaultOptions.overlapTriggerX
    );
    readonly hasBackdrop = input<boolean>(
        this.defaultOptions.hasBackdrop
    );
    readonly panelClass = input<string>('', { alias: 'class' });
    readonly backdropClass = input<string>(
        this.defaultOptions.backdropClass
    );
    readonly panelWidth = input<KbqPanelWidth>(
        this.defaultOptions.panelWidth ?? null
    );
    readonly panelMinWidth = input<KbqPanelMinWidth>(
        this.defaultOptions.panelMinWidth === undefined
            ? KBQ_PANEL_DEFAULT_MIN_WIDTH
            : this.defaultOptions.panelMinWidth
    );
    readonly panelMaxWidth = input<KbqPanelMaxWidth>(
        this.defaultOptions.panelMaxWidth === undefined ? null : this.defaultOptions.panelMaxWidth
    );
    readonly safeArea = input<boolean>(
        this.defaultOptions.safeArea ?? true
    );
    readonly closed = output<void | 'click' | 'keydown' | 'tab'>();
    parent: KbqDropdownPanel | undefined;
    direction: Direction;
    readonly items: Signal<readonly KbqDropdownItem[]>;
    hovered(): Observable<KbqDropdownItem>;
    handleKeydown(event: KeyboardEvent): void;
    focusFirstItem(origin?: FocusOrigin): void;
    resetActiveItem(): void;
    close(): void;
}
```

- `[activeDescendantNavigation]`: `boolean` — Whether the arrow keys move a highlight instead of the DOM focus. Turn it on when the keyboard belongs to
  something outside the panel — an editor whose caret the panel is anchored to, for instance — and forward
  the keys with `handleKeydown`.
- `[(xPosition)]`: `KbqDropdownPositionX` — Position of the dropdown in the X axis.
- `[(yPosition)]`: `KbqDropdownPositionY` — Position of the dropdown in the Y axis.
- `[(overlapTriggerY)]`: `boolean` — Whether the dropdown should overlap its trigger vertically.
- `[(overlapTriggerX)]`: `boolean` — Whether the dropdown should overlap its trigger horizontally.
- `[hasBackdrop]`: `boolean` — Whether the dropdown has a backdrop.
- `[class]`: `string` — Classes set on the host `kbq-dropdown` element, transferred onto the panel in the overlay container.
- `[backdropClass]`: `string` — Class to be added to the backdrop element.
- `[panelWidth]`: `KbqPanelWidth` — Width of the panel. If set to `auto`, the panel will match the trigger width, but will never be
  narrower than `panelMinWidth`. If set to null, the panel will grow to match its content.
  Any other value is used as an exact width, and `panelMinWidth` is not applied.
- `[panelMinWidth]`: `KbqPanelMinWidth` — Minimum width of the panel in pixels. The panel is never narrower than this, nor than its trigger.
- `[panelMaxWidth]`: `KbqPanelMaxWidth` — Maximum width of the panel in pixels. Caps how far the panel grows with its content — it never makes
  the panel narrower than the trigger, and never clamps an explicit `panelWidth`.
  When null, the `--kbq-dropdown-size-container-width-max` token applies.
- `[safeArea]`: `boolean` — Whether nested dropdowns opened from this dropdown's items use a "safe area": while the
  pointer moves from a trigger toward its open submenu, sibling items it crosses over on the way
  don't prematurely close the submenu. Set on the panel that contains the nested triggers.
- `(closed)`: `void | 'click' | 'keydown' | 'tab'` — Event emitted when the dropdown is closed.
- `parent`: `KbqDropdownPanel | undefined` — Parent dropdown of the current dropdown panel.
- `direction`: `Direction` — Layout direction of the dropdown.
- `items`: `Signal<readonly KbqDropdownItem[]>` — List of the items inside of a dropdown.
- `hovered()`: `Observable<KbqDropdownItem>` — Stream that emits whenever the hovered dropdown item changes.
- `handleKeydown()`: `void` — Handle a keyboard event from the dropdown, delegating to the appropriate action.
- `focusFirstItem()`: `void` — Applies the panel's initial focus, see `applyInitialFocus`.
  - `origin`: `FocusOrigin` — Action from which the focus originated. Used to set the correct styling.
- `resetActiveItem()`: `void` — Resets the active item in the dropdown. This is used when the dropdown is opened, allowing
  the user to start from the first option when pressing the down arrow.

#### KbqDropdownItem (component)

This directive is intended to be used inside an kbq-dropdown tag.

Note that it sets no ARIA role: neither the panel nor its items currently carry menu semantics, so
triggers must not advertise a popup they cannot describe.

```ts
@Component({
    selector: 'kbq-dropdown-item, [kbq-dropdown-item]',
    exportAs: 'kbqDropdownItem'
})
class KbqDropdownItem implements KbqTitleTextRef, KbqDropdownItemActionHost, IFocusableOption, Highlightable {
    readonly disabled = input<boolean>(false);
    readonly progress = input<boolean>(false);
    parentDropdownPanel?: KbqDropdownPanel | null;
    get textElement(): ElementRef;
    readonly icon: Signal<KbqIcon | undefined>;
    readonly itemAction: Signal<KbqDropdownItemAction | undefined>;
    readonly hovered: Subject<KbqDropdownItem>;
    readonly focused: Subject<KbqDropdownItem>;
    highlighted: boolean;
    isNested: boolean;
    resetStyles(): void;
    setActiveStyles(): void;
    setInactiveStyles(): void;
    focus(origin?: FocusOrigin, options?: FocusOptions): void;
    getHostElement(): HTMLElement;
    getTabIndex(): string;
    checkDisabled(event: Event): void;
    handleMouseEnter(): void;
    getLabel(): string;
    haltDisabledEvents(event: Event): void;
}
```

- `[disabled]`: `boolean` — Whether the dropdown item is disabled.
- `[progress]`: `boolean` — Whether the dropdown item is in a loading state.
- `textElement`: `ElementRef` — Element holding the text of the item, measured by `kbq-title`.
- `itemAction`: `Signal<KbqDropdownItemAction | undefined>` — Secondary, independently-focusable icon action projected into the item (e.g. a settings link).
- `hovered`: `Subject<KbqDropdownItem>` — Stream that emits when the dropdown item is hovered.
- `focused`: `Subject<KbqDropdownItem>` — Stream that emits when the menu item is focused.
- `highlighted`: `boolean` — Whether the dropdown item is highlighted.
- `isNested`: `boolean` — Whether the dropdown item acts as a trigger for a nested dropdown.
- `setActiveStyles()`: `void` — Styles the item as active without focusing it.
- `setInactiveStyles()`: `void` — Removes the styles applied by `setActiveStyles`.
- `focus()`: `void` — Focuses the dropdown item and reveals it.

  The reveal is explicit because the one `focus()` performs implicitly is not portable: WebKit defers
  it to a later rendering update, where it lands after — and undoes — any scrolling the reader did in
  the meantime. `preventScroll` is therefore always forced on, overriding `options`.
- `getHostElement()`: `HTMLElement` — Returns the host DOM element.
- `getTabIndex()`: `string` — Used to set the `tabindex`.
- `checkDisabled()`: `void` — Prevents the default element actions if it is disabled. Bound via `host` metadata.
- `handleMouseEnter()`: `void` — Emits to the hover stream. Bound via `host` metadata.
- `getLabel()`: `string` — Gets the label to be used when determining whether the option should be focused.

### Directives

#### KbqDropdownContent (directive)

Dropdown content that will be rendered lazily once the dropdown is opened.

```ts
@Directive({ selector: 'ng-template[kbqDropdownContent]' })
class KbqDropdownContent {
    attached: Subject<void>;
}
```

- `attached`: `Subject<void>` — Emits when the dropdown content has been attached.

#### KbqDropdownFooter (directive)

Footer that is rendered below the dropdown options panel.

```ts
@Directive({ selector: '[kbqDropdownFooter], kbq-dropdown-footer' })
class KbqDropdownFooter {}
```

#### KbqDropdownGroupHeader (directive)

Header of a group of items in the dropdown panel.

```ts
@Directive({ selector: '[kbqDropdownGroupHeader]' })
class KbqDropdownGroupHeader {}
```

#### KbqDropdownItemAction (directive)

Marks a secondary, independently-focusable/clickable element (e.g. a settings link) projected
inside a `kbq-dropdown-item` alongside the item's own primary action. Requires the item's host
element to be non-interactive (e.g. `<div kbq-dropdown-item>`), since interactive content
cannot validly nest inside another interactive element.

```ts
@Directive({ selector: '[kbqDropdownItemAction]' })
class KbqDropdownItemAction {
    getHostElement(): HTMLElement;
}
```

- `getHostElement()`: `HTMLElement` — Returns the host DOM element.

#### KbqDropdownItemCaption (directive)

Secondary line of a `kbq-dropdown-item`, shown under the item's text.

```ts
@Directive({ selector: '[kbqDropdownItemCaption]' })
class KbqDropdownItemCaption {}
```

#### KbqDropdownSearch (directive)

Marks a `kbq-form-field` projected into a `kbq-dropdown` as the panel's search field.

The panel keeps DOM focus in the field, so arrow keys move the highlighted item while the query
stays editable.

```ts
@Directive({ selector: '[kbqDropdownSearch]', exportAs: 'kbqDropdownSearch' })
class KbqDropdownSearch {
    get ngControl(): NgControl | null;
    focus(): void;
    reset(): void;
    value(): string;
}
```

- `ngControl`: `NgControl | null` — The form control the query is bound to.
- `focus()`: `void` — Focuses the search field.
- `reset()`: `void` — Clears the search query.
- `value()`: `string` — The current search query.

#### KbqDropdownStaticContent (directive)

```ts
@Directive({ selector: '[kbqDropdownStaticContent]' })
class KbqDropdownStaticContent {}
```

#### KbqDropdownTrigger (directive)

This directive is intended to be used in conjunction with an kbq-dropdown tag.  It is
responsible for toggling the display of the provided dropdown instance.

```ts
@Directive({
    selector: '[kbqDropdownTriggerFor]',
    exportAs: 'kbqDropdownTrigger'
})
class KbqDropdownTrigger implements KbqSiblingPopup {
    readonly offsetX = model<number | undefined>();
    readonly offsetY = input<number | undefined>();
    readonly data = input<any>(undefined, {
        alias: 'kbqDropdownTriggerData'
    });
    readonly openByArrowDown = model<boolean>(true);
    readonly origin = input<KbqOverlayOrigin | null>(null, {
        alias: 'kbqDropdownTriggerOrigin'
    });
    readonly autoFocus = input<boolean>(true, {
        alias: 'kbqDropdownTriggerAutoFocus'
    });
    readonly restoreFocus = model<boolean>(true, {
        alias: 'kbqDropdownTriggerRestoreFocus'
    });
    readonly dropdown = input<KbqDropdownPanel>(undefined, {
        alias: 'kbqDropdownTriggerFor'
    });
    readonly dropdownOpened = output<void>();
    readonly dropdownClosed = output<void>();
    lastDestroyReason: DropdownCloseReason;
    readonly openedChange: Observable<boolean>;
    openedBy: Exclude<FocusOrigin, 'program' | null> | undefined;
    get dir(): Direction;
    get opened(): boolean;
    get isAttached(): boolean;
    isNested(): boolean;
    toggle(): void;
    updatePosition(): void;
    open(): void;
    close(): void;
    focus(origin?: FocusOrigin, options?: FocusOptions): void;
    handleMousedown(event: MouseEvent): void;
    handleMouseLeave(event: MouseEvent): void;
    handleKeydown(event: KeyboardEvent): void;
    handleClick(event: MouseEvent): void;
}
```

- `[(offsetX)]`: `number | undefined` — Position offset of the dropdown in the X axis.
- `[offsetY]`: `number | undefined` — Position offset of the dropdown in the Y axis.
- `[kbqDropdownTriggerData]`: `any` — Data to be passed along to any lazily-rendered content.
- `[(openByArrowDown)]`: `boolean` — Whether the Down Arrow opens the dropdown when the trigger is focused. Written by `kbq-navbar-item`.
- `[kbqDropdownTriggerOrigin]`: `KbqOverlayOrigin | null` — What the panel is positioned against: an element, or a rectangle in viewport coordinates such as the one
  `kbqCreateCaretOrigin` keeps on the caret. `null` anchors the panel to the trigger element.

  Call `updatePosition` to move an open panel to a new origin.
- `[kbqDropdownTriggerAutoFocus]`: `boolean` — Whether opening the dropdown moves the keyboard focus into the panel. Turn it off to leave focus where it
  is — in the editor the panel is anchored to, for instance — and pair it with `activeDescendantNavigation`
  on the panel so the arrow keys still work.
- `[(kbqDropdownTriggerRestoreFocus)]`: `boolean` — Whether focus should be restored when the menu is closed.
  Note that disabling this option can have accessibility implications
  and it's up to you to manage focus, if you decide to turn it off.
- `[kbqDropdownTriggerFor]`: `KbqDropdownPanel` — References the dropdown instance that the trigger is associated with.
- `(dropdownOpened)`: `void` — Event emitted when the associated dropdown is opened.
- `(dropdownClosed)`: `void` — Event emitted when the associated dropdown is closed.
- `openedChange`: `Observable<boolean>` — Emits `true` when the dropdown opens and `false` when it closes. Part of the `KbqSiblingPopup` contract.
- `dir`: `Direction` — The text direction of the containing app.
- `opened`: `boolean` — Whether the dropdown is open.
- `isAttached`: `boolean` — Whether the dropdown overlay is currently attached. Part of the `KbqSiblingPopup` contract.

  Set right after the overlay is attached and cleared right after it is detached, once `destroy()` has
  restored focus to the trigger, so a tooltip on the trigger stays muted for that focus.
- `isNested()`: `boolean` — Whether the dropdown triggers a nested dropdown or a top-level one.
- `toggle()`: `void` — Toggles the dropdown between the open and closed states.
- `updatePosition()`: `void` — Moves an open panel to the current origin and re-applies its position.
- `open()`: `void` — Opens the dropdown.
- `close()`: `void` — Closes the dropdown.
- `focus()`: `void` — Focuses the dropdown trigger.
- `handleMousedown()`: `void` — Handles mouse presses on the trigger.
- `handleMouseLeave()`: `void` — Starts safe-area protection when the pointer leaves a trigger whose submenu is open, so a
  sibling item crossed on the way to the submenu doesn't prematurely close it.
- `handleKeydown()`: `void` — Handles key presses on the trigger.
- `handleClick()`: `void` — Handles click events on the trigger.

### Types

#### DropdownCloseReason (type)

Reason why the menu was closed.

```ts
type DropdownCloseReason = void | 'click' | 'keydown' | 'tab';
```

#### KbqDropdownDefaultOptions (interface)

Default `kbq-dropdown` options that can be overridden.

```ts
interface KbqDropdownDefaultOptions {
    xPosition: KbqDropdownPositionX;
    yPosition: KbqDropdownPositionY;
    overlapTriggerX: boolean;
    overlapTriggerY: boolean;
    backdropClass: string;
    hasBackdrop: boolean;
    safeArea?: boolean;
    panelWidth?: KbqPanelWidth;
    panelMinWidth?: KbqPanelMinWidth;
    panelMaxWidth?: KbqPanelMaxWidth;
}
```

- `xPosition`: `KbqDropdownPositionX` — The x-axis position of the dropdown.
- `yPosition`: `KbqDropdownPositionY` — The y-axis position of the dropdown.
- `overlapTriggerX`: `boolean` — Whether the dropdown should overlap the dropdown trigger horizontally.
- `overlapTriggerY`: `boolean` — Whether the dropdown should overlap the dropdown trigger vertically.
- `backdropClass`: `string` — Class to be applied to the dropdown's backdrop.
- `hasBackdrop`: `boolean` — Whether the dropdown has a backdrop.
- `safeArea?`: `boolean` — Whether nested dropdowns opened from this dropdown's items use a "safe area": while the
  pointer moves from a trigger toward its open submenu, sibling items it crosses over on the way
  don't prematurely close the submenu.
- `panelWidth?`: `KbqPanelWidth` — Width of the panel. If set to `auto`, the panel will match the trigger width.
  If set to null, the panel will grow to match its content.
- `panelMinWidth?`: `KbqPanelMinWidth` — Minimum width of the panel. If set to null, only the trigger width applies.
- `panelMaxWidth?`: `KbqPanelMaxWidth` — Maximum width of the panel. Caps growth by content only — it never overrides the trigger width or an
  explicit `panelWidth`. If null, the `--kbq-dropdown-size-container-width-max` token applies.

#### KbqDropdownPositionX (type)

Position of the dropdown panel along the x-axis.

```ts
type KbqDropdownPositionX = 'before' | 'after' | 'center';
```

#### KbqDropdownPositionY (type)

Position of the dropdown panel along the y-axis.

```ts
type KbqDropdownPositionY = 'above' | 'below';
```

### Constants

#### KBQ_DROPDOWN_DEFAULT_OPTIONS (const)

Injection token to be used to override the default options for `kbq-dropdown`.

```ts
const KBQ_DROPDOWN_DEFAULT_OPTIONS: InjectionToken<KbqDropdownDefaultOptions>;
```

#### KBQ_DROPDOWN_SCROLL_STRATEGY (const)

Injection token that determines the scroll handling while the dropdown is open. The root default keeps the
trigger usable outside `KbqDropdownModule`'s injector; providing the token anywhere still wins over it.

```ts
const KBQ_DROPDOWN_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
```

#### NESTED_HOVER_SWITCH_DELAY (const)

Grace period before switching to a different nested trigger hovered while a safe area is
protecting the currently open submenu. Without it, sweeping the pointer down a long list of
nested triggers on the way to the submenu would flicker each row's submenu open and closed.

```ts
const NESTED_HOVER_SWITCH_DELAY: number;
```

#### NESTED_PANEL_LEFT_PADDING (const)

```ts
const NESTED_PANEL_LEFT_PADDING: number;
```

#### NESTED_PANEL_TOP_PADDING (const)

Default top padding of the nested dropdown panel.

```ts
const NESTED_PANEL_TOP_PADDING: number;
```
