/**
 * Copyright Aquera Inc 2025
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { styles } from './nile-combobox.css';
import '../nile-icon';
import '../nile-popup/nile-popup';
import '../nile-tag/nile-tag';
import '../nile-option/nile-option';
import '../nile-checkbox/nile-checkbox';
import '../nile-loader/nile-loader';
import { animateTo, stopAnimations } from '../internal/animate';
import { defaultValue } from '../internal/default-value';
import { FormControlController } from '../internal/form';
import { getAnimation, setDefaultAnimation } from '../utilities/animation-registry';
import { HasSlotController } from '../internal/slot';
import { waitForEvent } from '../internal/event';
import { watch } from '../internal/watch';
import NileElement from '../internal/nile-element';
import { VirtualizerController } from '@tanstack/lit-virtual';
import { ComboboxSelectionManager } from './selection-manager.js';
import { ComboboxSearchManager } from './search-manager.js';
import { ComboboxRenderer } from './renderer.js';
import { ComboboxPortalManager } from './portal-manager.js';
import { hasGroups, flattenRows, filterRows, getOptionRows } from './group-utils.js';
import { VisibilityManager } from '../utilities/visibility-manager.js';
/**
 * @summary A data-driven combobox with virtualized options, inline search, multi-select tags,
 * custom value creation, and full WAI-ARIA Combobox keyboard navigation.
 *
 * @tag nile-combobox
 * @status stable
 * @since 2.0
 *
 * @dependency nile-icon
 * @dependency nile-popup
 * @dependency nile-tag
 * @dependency nile-checkbox
 * @dependency nile-loader
 *
 * @slot label - The input's label.
 * @slot prefix - Prepend a presentational icon or element before the input.
 * @slot clear-icon - An icon to use in lieu of the default clear icon.
 * @slot expand-icon - The icon to show when the control is expanded/collapsed.
 * @slot help-text - Text that describes how to use the input.
 * @slot footer - Custom footer content (overrides default footer in multi-select mode).
 * @slot no-results - Custom no-results content.
 *
 * @event nile-change - Emitted when the control's value changes.
 * @event nile-clear - Emitted when the control's value is cleared.
 * @event nile-input - Emitted when the control receives input.
 * @event nile-focus - Emitted when the control gains focus.
 * @event nile-blur - Emitted when the control loses focus.
 * @event nile-show - Emitted when the dropdown opens.
 * @event nile-after-show - Emitted after the dropdown opens and all animations complete.
 * @event nile-hide - Emitted when the dropdown closes.
 * @event nile-after-hide - Emitted after the dropdown closes and all animations complete.
 * @event nile-search - Emitted (debounced) when the user types. Useful for API-driven filtering.
 * @event nile-tag-remove - Emitted when a tag is removed in multi-select mode.
 * @event nile-tag-add - Emitted when a custom value is added via allowCustomValue.
 * @event nile-scroll-end - Emitted when scrolled to the bottom (for infinite loading).
 * @event nile-invalid - Emitted when form validation constraints aren't satisfied.
 * @event nile-select-all - Emitted when the Select all / Deselect all control toggles. Detail: { value, name, action: 'select-all' | 'deselect-all' }.
 *
 * @csspart form-control - The form control wrapper.
 * @csspart form-control-label - The label wrapper.
 * @csspart form-control-input - The input area wrapper.
 * @csspart combobox - The combobox trigger (input + tags + icons).
 * @csspart input - The text input element.
 * @csspart listbox - The dropdown listbox.
 * @csspart tags - The tags container in multi-select mode.
 * @csspart tag - Each individual tag.
 * @csspart clear-button - The clear button.
 * @csspart expand-icon - The expand/collapse icon.
 * @csspart top-actions - The sticky row above the option list that contains the Select all checkbox and the Selected / Show all filter toggle. Rendered only when multiple && selectAllEnabled.
 * @csspart select-all - The Select all / Deselect all checkbox wrapper inside top-actions.
 * @csspart show-toggle - The "Selected / Show all" filter button inside top-actions.
 * @csspart no-results - The empty-state container shown when a search/filter returns no items. Contains no-results-title and no-results-subtitle.
 * @csspart no-results-title - The title row of the no-results empty state.
 * @csspart no-results-subtitle - The subtitle row of the no-results empty state.
 * @csspart no-data - The empty-state container shown when the dataset is empty (no active search/filter).
 * @csspart footer - The footer with "Show Selected" / "Clear All".
 * @csspart no-results - The no-results message.
 */
let NileCombobox = class NileCombobox extends NileElement {
    constructor() {
        super(...arguments);
        this.formControlController = new FormControlController(this, {
            assumeInteractionOn: ['nile-blur', 'nile-input'],
        });
        this.hasSlotController = new HasSlotController(this, 'help-text', 'label');
        this.portalManager = new ComboboxPortalManager(this);
        this.searchManager = new ComboboxSearchManager();
        this.scrollElementRef = createRef();
        this.virtualizerCtrl = new VirtualizerController(this, {
            getScrollElement: () => this.scrollElementRef.value ?? null,
            count: 0,
            estimateSize: () => 38,
            overscan: 5,
        });
        this.hScrollElementRef = createRef();
        this.hVirtualizerCtrl = new VirtualizerController(this, {
            getScrollElement: () => this.hScrollElementRef.value ?? null,
            count: 0,
            estimateSize: () => 160,
            horizontal: true,
            overscan: 3,
        });
        this.scrolling = false;
        this.keyboardActiveIndex = -1;
        this.hasFocus = false;
        this.displayLabel = '';
        this.selectedOptions = [];
        /** The items displayed after filtering. Renderer reads from this. */
        this.filteredData = [];
        /**
         * Mixed (header + option) row list, only populated when `data` contains
         * group entries (`type: 'group'`). When non-empty, the listbox renders from
         * this instead of `filteredData`. `filteredData` stays in sync as the
         * option-only projection so existing select-all / strict-match / etc. logic
         * keeps working unchanged.
         */
        this.filteredRows = [];
        /** The complete unfiltered dataset (preserved for re-filtering). */
        this.originalData = [];
        this.showNoResults = false;
        this.showListbox = false;
        this.searchValue = '';
        this.showSelectedOnly = false;
        this.selectAllChecked = false;
        this.selectAllIndeterminate = false;
        /**
         * Index into `filteredRows` of the group header that should be pinned at
         * the top of the (virtualized) listbox right now. -1 means none.
         * Recomputed on scroll.
         */
        this.stickyHeaderIndex = -1;
        // ── Public properties ──
        this.name = '';
        this.data = [];
        this.value = '';
        this.defaultValue = '';
        this.size = 'medium';
        this.placeholder = 'Type to search...';
        this.multiple = false;
        this.label = '';
        this.required = false;
        this.disabled = false;
        this.open = false;
        this.clearable = false;
        this.loading = false;
        this.optionsLoading = false;
        /** When true, skip local filtering and rely solely on the `nile-search` event for API-driven results. */
        this.disableLocalSearch = false;
        /** When true, displays a "+ Add [value]" option for values not in the data. */
        this.allowCustomValue = false;
        /** When true, typing free text and pressing Enter/Tab adds it as a tag (like nile-chip's acceptUserInput). */
        this.acceptUserInput = false;
        /** When true, custom values added via allowCustomValue or acceptUserInput are also appended to the suggestions list. */
        this.addToSuggestions = false;
        /** When true, value must match an option. On blur, reverts to the last valid value if text doesn't match. */
        this.strict = false;
        /** Max tags visible before showing "+N more" (0 = no limit). */
        this.maxTagsVisible = 3;
        /** Controls how tags wrap in multi-select mode. */
        this.tagLayout = 'single-line';
        /**
         * Show footer with "Show Selected" and "Clear All" in multi-select mode.
         * Automatically suppressed when `selectAllEnabled` is true, since the top
         * actions row already provides the same controls.
         */
        this.showFooter = true;
        /**
         * When true (and `multiple` is true), renders a "Select all" / "Deselect all"
         * checkbox at the top of the listbox. Operates on the currently visible,
         * non-disabled options (respects the active search filter).
         */
        this.selectAllEnabled = false;
        /**
         * When true (default), data-driven group headers stick to the top of the
         * listbox while scrolling through that group's options (Atlassian-style).
         * Works in both plain and virtualized rendering modes. Set to false for
         * inline-only headers that scroll away with their options.
         */
        this.stickyGroupHeader = true;
        this.portal = false;
        this.hoist = false;
        this.placement = 'bottom';
        this.form = '';
        this.helpText = '';
        this.errorMessage = '';
        this.warning = false;
        this.error = false;
        this.success = false;
        this.filled = false;
        this.pill = false;
        this.noResultsMessage = 'No result found';
        this.noResultsSubtitle = 'Clear search or change filter';
        this.noDataMessage = 'No data available';
        /**
         * Pre-defined autocomplete suggestions. Accepts a JSON array string attribute
         * or a JS array property (like nile-chip). When `addToSuggestions` is true,
         * custom values added by the user are appended to this list and persisted.
         */
        this.autoCompleteOptions = [];
        /** Debounce interval (ms) for the nile-search event. */
        this.debounceMs = 300;
        this.allowHtmlLabel = true;
        this.enableVisibilityEffect = false;
        this.enableTabClose = false;
        this.noWidthSync = false;
        /** Number of columns in the dropdown grid (vertical scroll). When > 1, options render in a multi-column grid layout. */
        this.gridColumns = 1;
        /** Number of visible rows in horizontal grid mode. When > 0, enables horizontal virtual scroll with columns scrolling left/right. */
        this.gridRows = 0;
        /** Width of each column in horizontal grid mode (px). */
        this.gridColumnWidth = 160;
        this.lastVirtualizerGrouped = false;
    }
    // ── Accessors ──
    get validity() {
        return this.valueInput?.validity;
    }
    get validationMessage() {
        return this.valueInput?.validationMessage ?? '';
    }
    // ── Lifecycle ──
    connectedCallback() {
        super.connectedCallback();
        this.open = false;
        this.setupBoundHandlers();
        this.emit('nile-init');
        this.updateComplete.then(() => {
            if (this.autoCompleteOptions.length > 0 && this.data.length === 0) {
                this.data = [...this.autoCompleteOptions];
            }
            if (this.value && this.data.length > 0) {
                this.syncSelection();
            }
        });
    }
    disconnectedCallback() {
        this.removeOpenListeners();
        this.visibilityManager?.cleanup();
        this.searchManager.cancelDebounce();
        if (this.scrollTimeout) {
            clearTimeout(this.scrollTimeout);
            this.scrollTimeout = undefined;
        }
        this.portalManager.cleanupPortal();
    }
    firstUpdated(_changed) {
        this.visibilityManager = new VisibilityManager({
            host: this,
            target: this.combobox,
            enableVisibilityEffect: this.enableVisibilityEffect,
            enableTabClose: this.enableTabClose,
            isOpen: () => this.open,
            onAnchorOutOfView: () => {
                this.hide();
                this.emit('nile-visibility-change', { visible: false, reason: 'anchor-out-of-view', name: this.name });
            },
            onDocumentHidden: () => {
                this.hide();
                this.emit('nile-visibility-change', { visible: false, reason: 'document-hidden', name: this.name });
            },
            emit: (event, detail) => this.emit(`nile-${event}`, detail),
        });
    }
    updated(changedProperties) {
        if (changedProperties.has('value')) {
            this.syncSelection();
        }
        this.updateVirtualizerCount();
    }
    get isHorizontalGrid() {
        return this.gridRows > 0 && this.gridColumns <= 1;
    }
    /** True when the source data contains at least one group entry. */
    get hasGroupedData() {
        const base = this.originalData.length > 0 ? this.originalData : this.data;
        return hasGroups(base);
    }
    /**
     * Walk filteredRows and find the index of the deepest group header whose
     * virtual position is at or above `scrollTop`. That's the header that
     * should be pinned at the top of the listbox right now.
     */
    updateStickyHeader(scrollTop) {
        if (!this.stickyGroupHeader || !this.hasGroupedData) {
            if (this.stickyHeaderIndex !== -1)
                this.stickyHeaderIndex = -1;
            return;
        }
        let offset = 0;
        let stuck = -1;
        for (let i = 0; i < this.filteredRows.length; i++) {
            const row = this.filteredRows[i];
            const size = row.kind === 'header' ? 32 : 38;
            if (offset > scrollTop)
                break;
            if (row.kind === 'header')
                stuck = i;
            offset += size;
        }
        if (stuck !== this.stickyHeaderIndex)
            this.stickyHeaderIndex = stuck;
    }
    /** Recursively keep only options whose value is in `selectedSet`; drop empty groups. */
    pruneTreeBySelection(items, selectedSet) {
        const out = [];
        for (const item of items) {
            if (item && typeof item === 'object' && item.type === 'group' && Array.isArray(item.options)) {
                const kept = this.pruneTreeBySelection(item.options, selectedSet);
                if (kept.length > 0)
                    out.push({ ...item, options: kept });
            }
            else if (selectedSet.has(String(this.getItemValue(item)))) {
                out.push(item);
            }
        }
        return out;
    }
    get hasActiveFilter() {
        return !!this.searchValue || this.showSelectedOnly;
    }
    renderEmptyState() {
        if (this.hasActiveFilter) {
            return ComboboxRenderer.renderNoResults(this.noResultsMessage, this.noResultsSubtitle);
        }
        return ComboboxRenderer.renderNoData(this.noDataMessage);
    }
    get isBidirectionalGrid() {
        return this.gridRows > 0 && this.gridColumns > 1;
    }
    get virtualRowCount() {
        if (this.gridColumns > 1) {
            return Math.ceil(this.filteredData.length / this.gridColumns);
        }
        if (this.hasGroupedData) {
            return this.filteredRows.length;
        }
        return this.filteredData.length;
    }
    get virtualColumnCount() {
        return Math.ceil(this.filteredData.length / Math.max(this.gridRows, 1));
    }
    updateVirtualizerCount() {
        if (this.isHorizontalGrid) {
            const hVirtualizer = this.hVirtualizerCtrl.getVirtualizer();
            const colCount = this.virtualColumnCount;
            if (hVirtualizer.options.count !== colCount || hVirtualizer.options.estimateSize(0) !== this.gridColumnWidth) {
                hVirtualizer.setOptions({
                    ...hVirtualizer.options,
                    count: colCount,
                    estimateSize: () => this.gridColumnWidth,
                });
                hVirtualizer.measure();
            }
        }
        else {
            const virtualizer = this.virtualizerCtrl.getVirtualizer();
            const count = this.virtualRowCount;
            const grouped = this.hasGroupedData;
            const countChanged = virtualizer.options.count !== count;
            const modeChanged = this.lastVirtualizerGrouped !== grouped;
            if (countChanged || modeChanged) {
                const estimateSize = grouped
                    ? (index) => (this.filteredRows[index]?.kind === 'header' ? 32 : 38)
                    : () => 38;
                virtualizer.setOptions({
                    ...virtualizer.options,
                    count,
                    estimateSize,
                });
                virtualizer.measure();
                this.lastVirtualizerGrouped = grouped;
            }
        }
    }
    // ── Data helpers ──
    getDisplayText(item) {
        if (this.renderItemConfig?.getDisplayText)
            return this.renderItemConfig.getDisplayText(item);
        return item?.label || item?.name || item?.toString?.() || '';
    }
    getItemValue(item) {
        if (this.renderItemConfig?.getValue)
            return this.renderItemConfig.getValue(item);
        return item?.value ?? item;
    }
    getSearchText(item) {
        if (this.renderItemConfig?.getSearchText)
            return this.renderItemConfig.getSearchText(item);
        return this.getDisplayText(item);
    }
    getItemDescription(item) {
        return this.renderItemConfig?.getDescription?.(item) ?? item?.description ?? '';
    }
    getItemPrefix(item) {
        return this.renderItemConfig?.getPrefix?.(item) ?? item?.prefix ?? '';
    }
    getItemSuffix(item) {
        return this.renderItemConfig?.getSuffix?.(item) ?? item?.suffix ?? '';
    }
    // ── Selection ──
    syncSelection() {
        const baseData = this.originalData.length > 0 ? this.originalData : this.data;
        const items = this.hasGroupedData
            ? getOptionRows(flattenRows(baseData)).map(r => r.item)
            : baseData;
        this.selectedOptions = ComboboxSelectionManager.createOptionsFromValues(this.value, items, this.getDisplayText.bind(this), this.renderItemConfig?.getValue);
        if (!this.multiple) {
            const label = this.selectedOptions[0]?.getTextLabel() ?? '';
            this.displayLabel = label;
            if (label) {
                this.searchValue = label;
            }
            else if (!this.hasFocus) {
                this.searchValue = '';
            }
        }
        else {
            this.displayLabel = '';
        }
        this.updateValidity();
        this.updateSelectAllState();
    }
    // ── Select All ──
    /**
     * Returns the options eligible for Select All — the currently filtered data
     * (so the active search/filter is respected) minus any `disabled` items.
     * Mirrors nile-select's `getSelectableOptions()` contract.
     */
    getSelectableData() {
        return this.filteredData.filter((item) => !item?.disabled);
    }
    /**
     * Derives `selectAllChecked` + `selectAllIndeterminate` from the current
     * selection vs. the selectable set. No-op when `multiple` is false.
     *
     *   options=0            → both false (nothing to select, e.g. empty search)
     *   selected=0           → checked=false, indeterminate=false
     *   selected=options     → checked=true,  indeterminate=false
     *   otherwise (partial)  → checked=false, indeterminate=true
     */
    updateSelectAllState() {
        if (!this.multiple) {
            this.selectAllChecked = false;
            this.selectAllIndeterminate = false;
            return;
        }
        const options = this.getSelectableData();
        if (options.length === 0) {
            this.selectAllChecked = false;
            this.selectAllIndeterminate = false;
            return;
        }
        const selectedValues = Array.isArray(this.value) ? this.value : [];
        const selectedSet = new Set(selectedValues.map(String));
        const selectedInView = options.reduce((n, item) => {
            return n + (selectedSet.has(String(this.getItemValue(item))) ? 1 : 0);
        }, 0);
        if (selectedInView === 0) {
            this.selectAllChecked = false;
            this.selectAllIndeterminate = false;
        }
        else if (selectedInView === options.length) {
            this.selectAllChecked = true;
            this.selectAllIndeterminate = false;
        }
        else {
            this.selectAllChecked = false;
            this.selectAllIndeterminate = true;
        }
    }
    /**
     * Toggles all selectable, visible options. Indeterminate → clears selection
     * (matches the "Deselect All" label flip), empty → selects all,
     * fully-selected → clears.
     */
    handleSelectAllToggle(event) {
        event.stopPropagation();
        event.preventDefault();
        if (!this.multiple || !this.selectAllEnabled)
            return;
        const options = this.getSelectableData();
        if (options.length === 0)
            return;
        const shouldUnselectAll = this.selectAllChecked || this.selectAllIndeterminate;
        const current = Array.isArray(this.value) ? [...this.value] : [];
        const inViewValues = options.map(item => String(this.getItemValue(item)));
        const inViewSet = new Set(inViewValues);
        let next;
        if (shouldUnselectAll) {
            // Deselect only the in-view options; preserve selections outside the filter.
            next = current.filter(v => !inViewSet.has(String(v)));
        }
        else {
            // Select all in-view options on top of any existing (out-of-view) selection.
            const merged = new Set(current.map(String));
            inViewValues.forEach(v => merged.add(v));
            next = Array.from(merged);
        }
        this.value = next;
        this.searchValue = '';
        this.syncSelection();
        this.filterOptions('', true);
        this.updateComplete.then(() => {
            this.emit('nile-input', { value: this.value, name: this.name });
            this.emit('nile-change', { value: this.value, name: this.name });
            this.emit('nile-select-all', {
                value: this.value,
                name: this.name,
                action: shouldUnselectAll ? 'deselect-all' : 'select-all',
            });
        });
    }
    setupBoundHandlers() {
        this.handleDocumentFocusIn = (event) => {
            if (!this.open)
                return;
            const path = event.composedPath();
            const hitSelf = path.includes(this);
            const hitPopup = this.popup && path.includes(this.popup);
            const hitPortal = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
            if (!hitSelf && !hitPopup && !hitPortal)
                this.hide();
        };
        this.handleDocumentKeyDown = (event) => {
            if (!this.open && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter')
                return;
            this.onGlobalKeyDown(event);
        };
        this.handleDocumentMouseDown = (event) => {
            if (!this.open)
                return;
            const path = event.composedPath();
            const hitSelf = path.includes(this);
            const hitPopup = this.popup && path.includes(this.popup);
            const hitPortal = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
            if (!hitSelf && !hitPopup && !hitPortal)
                this.hide();
        };
        this.handleWindowError = (event) => {
            const msg = event.error?.message || event.message || '';
            if (msg.includes("Cannot read properties of null (reading 'insertBefore')")) {
                event.preventDefault();
            }
        };
        this.handleWindowResize = () => this.portalManager.updatePosition();
        this.handleWindowScroll = () => this.portalManager.updatePosition();
    }
    addOpenListeners() {
        document.addEventListener('focusin', this.handleDocumentFocusIn);
        document.addEventListener('keydown', this.handleDocumentKeyDown);
        document.addEventListener('mousedown', this.handleDocumentMouseDown);
        window.addEventListener('error', this.handleWindowError);
        if (this.portal) {
            window.addEventListener('resize', this.handleWindowResize);
            window.addEventListener('scroll', this.handleWindowScroll, true);
        }
    }
    removeOpenListeners() {
        document.removeEventListener('focusin', this.handleDocumentFocusIn);
        document.removeEventListener('keydown', this.handleDocumentKeyDown);
        document.removeEventListener('mousedown', this.handleDocumentMouseDown);
        window.removeEventListener('error', this.handleWindowError);
        window.removeEventListener('resize', this.handleWindowResize);
        window.removeEventListener('scroll', this.handleWindowScroll, true);
    }
    // ── Keyboard navigation (WAI-ARIA Combobox) ──
    onInputKeyDown(event) {
        switch (event.key) {
            case 'ArrowDown':
                event.preventDefault();
                if (!this.open) {
                    this.show();
                }
                else {
                    this.moveActiveOption(this.gridColumns > 1 ? this.gridColumns : 1);
                }
                break;
            case 'ArrowUp':
                event.preventDefault();
                if (!this.open) {
                    this.show();
                }
                else {
                    this.moveActiveOption(this.gridColumns > 1 ? -this.gridColumns : -1);
                }
                break;
            case 'ArrowRight':
                if (this.gridColumns > 1 && this.open) {
                    event.preventDefault();
                    this.moveActiveOption(1);
                }
                break;
            case 'ArrowLeft':
                if (this.gridColumns > 1 && this.open) {
                    event.preventDefault();
                    this.moveActiveOption(-1);
                }
                break;
            case 'Enter':
                event.preventDefault();
                if (this.open) {
                    this.selectActiveOption();
                }
                else if (this.searchValue.trim() && (this.acceptUserInput || this.allowCustomValue)) {
                    this.addCustomValue(this.searchValue.trim());
                }
                else {
                    this.show();
                }
                break;
            case 'Escape':
                if (this.open) {
                    event.preventDefault();
                    event.stopPropagation();
                    this.hide();
                }
                break;
            case 'Home':
                if (this.open) {
                    event.preventDefault();
                    this.setActiveOption(0);
                }
                break;
            case 'End':
                if (this.open) {
                    event.preventDefault();
                    this.setActiveOption(this.filteredData.length - 1);
                }
                break;
            case 'Backspace':
                if (this.multiple && this.searchValue === '' && this.selectedOptions.length > 0) {
                    const last = this.selectedOptions[this.selectedOptions.length - 1];
                    this.removeTag(last);
                }
                break;
            case 'Tab':
                if (this.multiple && this.acceptUserInput && this.searchValue.trim()) {
                    this.addCustomValue(this.searchValue.trim());
                }
                if (this.open)
                    this.hide();
                break;
        }
    }
    onGlobalKeyDown(event) {
        if (event.key === 'Escape' && this.open) {
            event.preventDefault();
            event.stopPropagation();
            this.hide();
            this.inputElement?.focus({ preventScroll: true });
        }
    }
    getVisibleOptions() {
        const root = this.portal ? this.portalManager.portalContainerElement : this.shadowRoot;
        if (!root)
            return [];
        return root.querySelectorAll('nile-option:not([disabled])');
    }
    moveActiveOption(delta) {
        const options = this.getVisibleOptions();
        if (!options.length)
            return;
        let next = this.keyboardActiveIndex + delta;
        if (next < 0)
            next = options.length - 1;
        if (next >= options.length)
            next = 0;
        this.setActiveOption(next);
    }
    setActiveOption(index) {
        const options = this.getVisibleOptions();
        if (!options.length)
            return;
        options.forEach((opt, i) => {
            const el = opt;
            if (i === index) {
                el.setAttribute('aria-current', 'true');
                el.classList.add('combobox__option--active');
                el.scrollIntoView?.({ block: 'nearest' });
            }
            else {
                el.removeAttribute('aria-current');
                el.classList.remove('combobox__option--active');
            }
        });
        this.keyboardActiveIndex = index;
    }
    selectActiveOption() {
        const options = this.getVisibleOptions();
        if (this.keyboardActiveIndex >= 0 && this.keyboardActiveIndex < options.length) {
            const opt = options[this.keyboardActiveIndex];
            if (opt && !opt.disabled) {
                this.handleOptionSelection(opt.value);
            }
        }
        else if (this.searchValue.trim() && (this.allowCustomValue || this.acceptUserInput)) {
            this.addCustomValue(this.searchValue.trim());
        }
    }
    // ── Input handling ──
    onInputChange(event) {
        event.stopPropagation();
    }
    onInputHandler(event) {
        const target = event.target;
        this.searchValue = target.value;
        if (!this.open)
            this.show();
        this.emit('nile-input', { value: this.searchValue, name: this.name });
        if (this.disableLocalSearch) {
            this.searchManager.debounceSearch((q) => this.emit('nile-search', { query: q, name: this.name }), this.searchValue, this.debounceMs);
        }
        else {
            this.filterOptions(this.searchValue);
        }
        this.keyboardActiveIndex = -1;
    }
    filterOptions(search, preserveScroll = false) {
        const baseData = this.originalData.length > 0 ? this.originalData : this.data;
        if (this.hasGroupedData) {
            // Grouped path: filter the tree, derive options-only projection.
            let tree = baseData;
            if (this.showSelectedOnly) {
                const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
                const selectedSet = new Set(selectedValues.map(v => String(v)));
                tree = this.pruneTreeBySelection(baseData, selectedSet);
            }
            const { rows } = filterRows(tree, search, this.getSearchText.bind(this));
            this.filteredRows = rows;
            this.filteredData = getOptionRows(rows).map(r => r.item);
            this.showNoResults = this.filteredData.length === 0;
            // Recompute sticky header at current scroll (defaults to top on filter).
            const st = this.scrollElementRef.value?.scrollTop ?? 0;
            this.updateStickyHeader(st);
        }
        else {
            let source = baseData;
            if (this.showSelectedOnly) {
                const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
                const selectedSet = new Set(selectedValues.map(v => String(v)));
                source = baseData.filter((item) => selectedSet.has(String(this.getItemValue(item))));
            }
            const { filteredItems, showNoResults } = this.searchManager.filter(search, source, this.getSearchText.bind(this));
            this.filteredData = filteredItems;
            this.filteredRows = [];
            this.showNoResults = showNoResults;
        }
        this.portalManager.resetMeasuredHeight();
        if (!preserveScroll) {
            this.resetScrollPosition();
        }
        this.updateSelectAllState();
        this.requestUpdate();
    }
    // ── Focus ──
    onFocusIn() {
        this.hasFocus = true;
        this.emit('nile-focus');
        if (!this.multiple && this.displayLabel) {
            this.searchValue = this.displayLabel;
            this.requestUpdate();
            this.updateComplete.then(() => {
                this.inputElement?.select();
            });
        }
    }
    onFocusOut() {
        this.hasFocus = false;
        this.emit('nile-blur');
        if (this.open)
            return;
        if (!this.multiple) {
            if (this.strict) {
                const baseData = this.originalData.length > 0 ? this.originalData : this.data;
                const allItems = this.hasGroupedData
                    ? getOptionRows(flattenRows(baseData)).map(r => r.item)
                    : baseData;
                const match = allItems.find((item) => this.getDisplayText(item).toLowerCase() === this.searchValue.toLowerCase());
                if (match) {
                    const val = this.getItemValue(match);
                    if (this.value !== val) {
                        this.value = val;
                        this.syncSelection();
                        this.emit('nile-change', { value: this.value, name: this.name });
                    }
                }
                else {
                    this.searchValue = this.displayLabel;
                }
            }
            else {
                this.searchValue = this.displayLabel;
            }
        }
    }
    // ── Combobox trigger interaction ──
    onTriggerMouseDown(event) {
        if (this.disabled)
            return;
        const path = event.composedPath();
        if (path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button'))
            return;
        event.preventDefault();
        this.inputElement?.focus({ preventScroll: true });
        if (this.open) {
            this.hide();
        }
        else {
            this.show();
        }
    }
    // ── Option click ──
    onOptionClick(event) {
        // Use composedPath to find nile-option across shadow boundaries
        const path = event.composedPath();
        const option = path.find((el) => el.tagName === 'NILE-OPTION');
        if (!option || option.disabled)
            return;
        this.handleOptionSelection(option.value);
    }
    handleOptionSelection(optionValue) {
        const oldValue = this.value;
        if (this.multiple) {
            const current = Array.isArray(this.value) ? this.value : [];
            this.value = ComboboxSelectionManager.toggleMultiValue(current, optionValue);
            this.searchValue = '';
            this.syncSelection();
            this.filterOptions('', true);
        }
        else {
            this.value = optionValue;
            this.syncSelection();
            this.searchValue = this.displayLabel;
            this.hide();
        }
        if (JSON.stringify(this.value) !== JSON.stringify(oldValue)) {
            this.updateComplete.then(() => {
                this.emit('nile-input', { value: this.value, name: this.name });
                this.emit('nile-change', { value: this.value, name: this.name });
            });
        }
        this.inputElement?.focus({ preventScroll: true });
    }
    addCustomValue(val) {
        if (this.addToSuggestions) {
            const allItems = this.originalData.length > 0 ? this.originalData : this.data;
            const alreadyExists = allItems.some((item) => String(this.getItemValue(item)) === val || this.getDisplayText(item) === val);
            if (!alreadyExists) {
                const newItem = this.renderItemConfig?.getValue
                    ? { value: val, label: val }
                    : val;
                this.originalData = [...allItems, newItem];
                this.data = [...this.originalData];
                this.autoCompleteOptions = [...this.originalData];
                this.filteredData = [...this.originalData];
            }
        }
        if (this.multiple) {
            const current = Array.isArray(this.value) ? this.value : [];
            if (!current.includes(val)) {
                this.value = [...current, val];
            }
        }
        else {
            this.value = val;
            this.hide();
        }
        this.searchValue = this.multiple ? '' : val;
        this.syncSelection();
        this.filterOptions(this.searchValue);
        this.emit('nile-tag-add', { value: val, name: this.name });
        this.emit('nile-change', { value: this.value, name: this.name });
    }
    // ── Clear ──
    onClearClick(event) {
        event.stopPropagation();
        this.value = this.multiple ? [] : '';
        this.searchValue = '';
        this.syncSelection();
        this.filterOptions('');
        this.emit('nile-clear', { value: this.value, name: this.name });
        this.emit('nile-change', { value: this.value, name: this.name });
    }
    // ── Tags ──
    removeTag(option) {
        if (this.disabled)
            return;
        const current = Array.isArray(this.value) ? this.value : [];
        this.value = ComboboxSelectionManager.removeValue(current, option.value);
        this.syncSelection();
        this.emit('nile-tag-remove', { value: this.value, name: this.name, removedtagvalue: option.value });
        this.emit('nile-change', { value: this.value, name: this.name });
    }
    // ── Footer ──
    onFooterClick(event) {
        event.stopPropagation();
        event.preventDefault();
    }
    toggleShowSelected(event) {
        event.stopPropagation();
        event.preventDefault();
        if (this.selectedOptions.length === 0)
            return;
        this.showSelectedOnly = !this.showSelectedOnly;
        if (this.showSelectedOnly)
            this.searchValue = '';
        this.filterOptions(this.searchValue);
    }
    clearAll() {
        this.showSelectedOnly = false;
        this.value = this.multiple ? [] : '';
        this.filterOptions('');
        this.syncSelection();
        this.emit('nile-change', { value: this.value, name: this.name });
        this.emit('nile-clear', { value: this.value, name: this.name });
        this.resetScrollPosition();
    }
    // ── Scroll ──
    onScroll(e) {
        const target = e.target;
        this.updateStickyHeader(target.scrollTop);
        if (this.showSelectedOnly)
            return;
        this.emit('nile-scroll', { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, name: this.name });
        if (!this.scrolling) {
            this.scrolling = true;
            this.emit('nile-scroll-start', { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, name: this.name });
        }
        clearTimeout(this.scrollTimeout);
        this.scrollTimeout = window.setTimeout(() => {
            this.scrolling = false;
        }, 300);
        const isAtBottom = Math.ceil(target.scrollTop) >= Math.floor(target.scrollHeight - target.offsetHeight);
        if (isAtBottom) {
            this.emit('nile-scroll-end', { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, name: this.name, isAtBottom: true });
        }
    }
    // ── Show / Hide ──
    async show() {
        if (this.open || this.disabled)
            return undefined;
        this.open = true;
        return waitForEvent(this, 'nile-after-show');
    }
    async hide() {
        if (!this.open)
            return undefined;
        this.open = false;
        return waitForEvent(this, 'nile-after-hide');
    }
    async handleOpenChange() {
        if (this.open && !this.disabled) {
            this.visibilityManager?.setup();
            this.showListbox = true;
            await this.updateComplete;
            await this.doOpen();
            if (this.portal)
                this.portalManager.setupPortal();
        }
        else {
            this.visibilityManager?.cleanup();
            await this.doClose();
            this.showListbox = false;
            if (this.portal)
                this.portalManager.cleanupPortal();
        }
    }
    async doOpen() {
        this.emit('nile-show', { value: this.value, name: this.name });
        this.addOpenListeners();
        this.keyboardActiveIndex = -1;
        if (this.originalData.length === 0 && this.data.length > 0) {
            this.originalData = [...this.data];
        }
        if (this.hasGroupedData) {
            this.filterOptions(this.searchValue);
        }
        else {
            this.filteredData = [...(this.originalData.length > 0 ? this.originalData : this.data)];
            this.filteredRows = [];
            this.showNoResults = this.filteredData.length === 0;
        }
        await stopAnimations(this);
        if (this.popup?.popup) {
            this.popup.popup.style.visibility = 'hidden';
        }
        this.popup.active = true;
        await new Promise(r => requestAnimationFrame(r));
        await new Promise(r => requestAnimationFrame(r));
        if (this.popup?.popup) {
            this.popup.popup.style.visibility = '';
        }
        const { keyframes, options } = getAnimation(this, 'combobox.show', { dir: 'ltr' });
        await animateTo(this.popup.popup, keyframes, options);
        this.resetScrollPosition();
        this.emit('nile-after-show', { value: this.value, name: this.name });
    }
    async doClose() {
        this.emit('nile-hide', { value: this.value, name: this.name });
        this.removeOpenListeners();
        await stopAnimations(this);
        const { keyframes, options } = getAnimation(this, 'combobox.hide', { dir: 'ltr' });
        await animateTo(this.popup.popup, keyframes, options);
        this.popup.active = false;
        if (this.popup?.popup)
            this.popup.popup.style.visibility = '';
        this.showSelectedOnly = false;
        this.portalManager.resetMeasuredHeight();
        if (!this.multiple) {
            this.searchValue = this.displayLabel;
        }
        this.emit('nile-after-hide', { value: this.value, name: this.name });
    }
    // ── Watchers ──
    handleDisabledChange() {
        if (this.disabled) {
            this.open = false;
            this.handleOpenChange();
        }
    }
    handleValueChange() {
        this.syncSelection();
        this.requestUpdate();
        if (this.portal && this.portalManager.portalContainerElement) {
            this.portalManager.updatePosition();
        }
    }
    handleDataChange() {
        if (this.data.length > 0 && !this.showSelectedOnly) {
            this.originalData = [...this.data];
        }
        if (this.hasGroupedData) {
            this.filterOptions(this.searchValue);
        }
        else {
            this.filteredData = [...this.data];
            this.filteredRows = [];
        }
        this.syncSelection();
        this.updateSelectAllState();
        if (!this.optionsLoading && !this.loading && this.data.length === 0) {
            this.showNoResults = true;
        }
        else if (this.data.length > 0) {
            this.showNoResults = false;
        }
        this.portalManager.resetMeasuredHeight();
        if (this.portal && this.portalManager.portalContainerElement) {
            this.portalManager.updatePosition();
        }
        this.requestUpdate();
    }
    handleAutoCompleteOptionsChange() {
        if (this.autoCompleteOptions.length > 0) {
            this.data = [...this.autoCompleteOptions];
        }
    }
    handleSelectAllConfigChange() {
        this.updateSelectAllState();
    }
    handlePortalChange() {
        if (this.open) {
            if (this.portal)
                this.portalManager.setupPortal();
            else
                this.portalManager.cleanupPortal();
        }
    }
    // ── Form integration ──
    checkValidity() { return this.valueInput.checkValidity(); }
    getForm() { return this.formControlController.getForm(); }
    reportValidity() { return this.valueInput.reportValidity(); }
    setCustomValidity(message) {
        this.valueInput.setCustomValidity(message);
        this.formControlController.updateValidity();
    }
    focus(options) { this.inputElement?.focus(options); }
    blur() { this.inputElement?.blur(); }
    handleInvalid(event) {
        this.formControlController.setValidity(false);
        this.formControlController.emitInvalidEvent(event);
    }
    updateValidity() {
        this.updateComplete.then(() => this.formControlController.updateValidity());
    }
    async resetScrollPosition() {
        await this.portalManager.resetScrollPosition();
    }
    // ── Render ──
    render() {
        const hasLabelSlot = this.hasSlotController.test('label');
        const hasLabel = this.label ? true : !!hasLabelSlot;
        const hasValue = this.multiple
            ? (Array.isArray(this.value) && this.value.length > 0)
            : (typeof this.value === 'string' && this.value !== '');
        const hasClearIcon = this.clearable && !this.disabled && hasValue;
        const hasHelpText = !!this.helpText;
        const hasErrorMessage = !!this.errorMessage;
        const isEmpty = !hasValue;
        return html `
      <div
        part="form-control"
        class=${classMap({
            'form-control': true,
            'form-control--small': this.size === 'small',
            'form-control--medium': this.size === 'medium',
            'form-control--large': this.size === 'large',
            'form-control--has-label': hasLabel,
            'form-control--has-help-text': hasHelpText,
        })}
      >
        <label
          id="label"
          part="form-control-label"
          class="form-control__label"
          aria-hidden=${hasLabel ? 'false' : 'true'}
          @click=${() => this.inputElement?.focus()}
        >
          <slot name="label">${this.label}</slot>
        </label>

        <div part="form-control-input" class="form-control-input">
          <nile-popup
            class=${classMap({
            'combobox-popup': true,
            'combobox--open': this.open,
            'combobox--closed': !this.open,
            'combobox--disabled': this.disabled,
            'combobox--focused': this.hasFocus,
            'combobox--multiple': this.multiple,
            'combobox--empty': isEmpty,
            'combobox--small': this.size === 'small',
            'combobox--medium': this.size === 'medium',
            'combobox--large': this.size === 'large',
            'combobox--warning': this.warning,
            'combobox--error': this.error,
            'combobox--success': this.success,
            'combobox--filled': this.filled,
            'combobox--pill': this.pill,
        })}
            placement=${this.placement}
            strategy=${this.portal ? 'fixed' : (this.hoist ? 'fixed' : 'absolute')}
            distance="6"
            ?flip=${!this.portal}
            ?shift=${!this.portal}
            sync=${ifDefined(!this.noWidthSync ? 'width' : undefined)}
            auto-size="vertical"
            auto-size-padding="10"
          >
            ${this.renderTrigger(hasClearIcon)}
            ${this.showListbox ? this.renderListbox() : html ``}
          </nile-popup>

          ${hasHelpText ? html `<nile-form-help-text>${this.helpText}</nile-form-help-text>` : html ``}
          ${hasErrorMessage ? html `<nile-form-error-message>${this.errorMessage}</nile-form-error-message>` : html ``}
        </div>
      </div>
    `;
    }
    renderTrigger(hasClearIcon) {
        const hasTags = this.multiple && this.selectedOptions.length > 0;
        const inputPlaceholder = hasTags ? '' : this.placeholder;
        return html `
      <div
        part="combobox"
        class="combobox__trigger"
        slot="anchor"
        @mousedown=${this.onTriggerMouseDown}
      >
        <slot part="prefix" name="prefix" class="combobox__prefix"></slot>

        <div class=${classMap({
            'combobox__scroll-area': true,
            'combobox__scroll-area--single-line': this.multiple && this.tagLayout === 'single-line',
            'combobox__scroll-area--wrap': this.multiple && (this.tagLayout === 'wrap' || this.tagLayout === 'fixed-height'),
        })}>
          ${hasTags ? this.renderInlineTags() : html ``}

          <div class="combobox__input-wrapper">
            <input
              part="input"
              class="combobox__input"
              type="text"
              placeholder=${inputPlaceholder}
              .disabled=${this.disabled}
              .value=${this.searchValue}
              autocomplete="off"
              spellcheck="false"
              autocapitalize="off"
              aria-controls="listbox"
              aria-expanded=${this.open ? 'true' : 'false'}
              aria-haspopup="listbox"
              aria-labelledby="label"
              aria-disabled=${this.disabled ? 'true' : 'false'}
              aria-describedby="help-text"
              aria-activedescendant=${this.keyboardActiveIndex >= 0 ? `option-${this.keyboardActiveIndex}` : ''}
              role="combobox"
              tabindex="0"
              @input=${this.onInputHandler}
              @keydown=${this.onInputKeyDown}
              @focus=${this.onFocusIn}
              @blur=${this.onFocusOut}
              @change=${this.onInputChange}
            />
          </div>
        </div>

        <input
          class="combobox__value-input"
          type="text"
          ?disabled=${this.disabled}
          ?required=${this.required}
          .value=${Array.isArray(this.value) ? this.value.join(', ') : this.value}
          tabindex="-1"
          aria-hidden="true"
          @focus=${() => this.focus()}
          @invalid=${this.handleInvalid}
        />

        <div class="combobox__actions">
          ${hasClearIcon ? this.renderClearButton() : html ``}

          <slot name="expand-icon" part="expand-icon" class="combobox__expand-icon">
            <nile-icon
              library="system"
              name="var(--nile-icon-arrow-down, var(--ng-icon-chevron-down))"
              method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
              color="var(--nile-colors-dark-500, var(--ng-colors-fg-quaternary-400))"
            ></nile-icon>
          </slot>
        </div>
      </div>
    `;
    }
    renderInlineTags() {
        const tags = [];
        this.selectedOptions.forEach((option, index) => {
            if (this.maxTagsVisible > 0 && index >= this.maxTagsVisible) {
                if (index === this.maxTagsVisible) {
                    tags.push(html `<span class="combobox__tags-count">+${this.selectedOptions.length - this.maxTagsVisible} More</span>`);
                }
                return;
            }
            tags.push(html `
        <nile-tag
          part="tag"
          exportparts="base:tag__base, content:tag__content, remove-button:tag__remove-button"
          ?pill=${this.pill}
          size=${this.size}
          removable
          ?disabled=${this.disabled}
          @nile-remove=${(e) => { e.stopPropagation(); this.removeTag(option); }}
        >
          ${option.getTextLabel()}
        </nile-tag>
      `);
        });
        return tags;
    }
    renderClearButton() {
        return html `
      <button
        part="clear-button"
        class="combobox__clear"
        type="button"
        aria-label="Clear"
        @mousedown=${(e) => e.stopPropagation()}
        @click=${this.onClearClick}
        tabindex="-1"
      >
        <slot name="clear-icon">
          <nile-icon
            name="var(--nile-icon-close, var(--ng-icon-x-close))"
            size="var(--nile-height-14px, var(--ng-height-16px))"
            method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
            library="system"
            color="var(--nile-colors-dark-500, var(--ng-colors-fg-quaternary-400))"
          ></nile-icon>
        </slot>
      </button>
    `;
    }
    renderStickyHeaderOverlay(grouped, useVirtual) {
        if (!this.stickyGroupHeader || !grouped || !useVirtual)
            return html ``;
        const idx = this.stickyHeaderIndex;
        if (idx < 0 || idx >= this.filteredRows.length)
            return html ``;
        const row = this.filteredRows[idx];
        if (row.kind !== 'header')
            return html ``;
        return html `
      <div class="combobox__group-sticky-overlay">
        ${ComboboxRenderer.renderGroupHeader(row)}
      </div>
    `;
    }
    renderListbox() {
        const showAddOption = this.allowCustomValue
            && this.searchValue.trim()
            && !this.filteredData.some((item) => {
                const v = this.getItemValue(item);
                return String(v).toLowerCase() === this.searchValue.trim().toLowerCase();
            })
            && !this.filteredData.some((item) => {
                const t = this.getDisplayText(item);
                return t.toLowerCase() === this.searchValue.trim().toLowerCase();
            });
        if (this.isHorizontalGrid) {
            return this.renderHorizontalListbox(!!showAddOption);
        }
        const isGrid = this.gridColumns > 1 || this.isBidirectionalGrid;
        const grouped = this.hasGroupedData && !isGrid;
        const useVirtual = grouped
            ? this.filteredRows.length >= 5
            : ComboboxRenderer.shouldUseVirtualizer(this.filteredData, this.gridColumns);
        const virtualizer = this.virtualizerCtrl.getVirtualizer();
        const virtualItems = (useVirtual || isGrid) ? virtualizer.getVirtualItems() : [];
        const totalSize = (useVirtual || isGrid) ? virtualizer.getTotalSize() : 0;
        return html `
      <div
        id="listbox"
        role="listbox"
        aria-expanded=${this.open ? 'true' : 'false'}
        aria-multiselectable=${this.multiple ? 'true' : 'false'}
        aria-labelledby="label"
        part="listbox"
        class="combobox__listbox ${this.isBidirectionalGrid ? 'combobox__listbox--bidirectional' : ''} ${this.portal ? 'combobox__portal-hidden' : ''}"
        tabindex="-1"
        @mouseup=${this.onOptionClick}
        @scroll=${this.onScroll}
        style="${this.portal ? 'display: none;' : ''}${this.isBidirectionalGrid ? `max-height:${this.gridRows * 38 + 16}px;` : ''}"
        ${ref(this.scrollElementRef)}
      >
        ${this.renderLoader()}
        ${this.renderSelectAll()}
        ${this.renderStickyHeaderOverlay(grouped, useVirtual)}
        ${this.showNoResults && !this.optionsLoading && !this.loading
            ? this.renderEmptyState()
            : isGrid
                ? ComboboxRenderer.renderVirtualizedGrid(virtualItems, totalSize, this.filteredData, this.value, this.multiple, this.gridColumns, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.optionsLoading || this.loading, this.allowHtmlLabel, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined, this.isBidirectionalGrid ? this.gridColumnWidth : undefined)
                : grouped
                    ? (useVirtual
                        ? ComboboxRenderer.renderRowsVirtualized(virtualItems, totalSize, this.filteredRows, this.value, this.multiple, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.optionsLoading || this.loading, this.allowHtmlLabel, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined)
                        : ComboboxRenderer.renderRowsPlain(this.filteredRows, this.value, this.multiple, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.showNoResults, this.noResultsMessage, this.optionsLoading || this.loading, this.onScroll.bind(this), this.allowHtmlLabel, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined, undefined, this.hasActiveFilter ? this.noResultsSubtitle : undefined))
                    : useVirtual
                        ? ComboboxRenderer.renderVirtualizedOptions(virtualItems, totalSize, this.filteredData, this.value, this.multiple, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.optionsLoading || this.loading, this.allowHtmlLabel, virtualizer.measureElement, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined)
                        : ComboboxRenderer.renderPlainOptions(this.filteredData, this.value, this.multiple, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.showNoResults, this.noResultsMessage, this.optionsLoading || this.loading, this.onScroll.bind(this), this.allowHtmlLabel, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined, undefined, this.hasActiveFilter ? this.noResultsSubtitle : undefined)}
        ${showAddOption ? html `
          <div @mouseup=${(e) => { e.stopPropagation(); this.addCustomValue(this.searchValue.trim()); }}>
            ${ComboboxRenderer.renderAddCustomOption(this.searchValue.trim(), this.multiple)}
          </div>
        ` : ''}
        ${this.multiple && this.showFooter && !this.selectAllEnabled ? this.renderFooter() : ''}
      </div>
    `;
    }
    renderHorizontalListbox(showAddOption) {
        const hVirtualizer = this.hVirtualizerCtrl.getVirtualizer();
        const virtualItems = hVirtualizer.getVirtualItems();
        const totalSize = hVirtualizer.getTotalSize();
        return html `
      <div
        id="listbox"
        role="listbox"
        aria-expanded=${this.open ? 'true' : 'false'}
        aria-multiselectable=${this.multiple ? 'true' : 'false'}
        aria-labelledby="label"
        part="listbox"
        class="combobox__listbox combobox__listbox--horizontal ${this.portal ? 'combobox__portal-hidden' : ''}"
        tabindex="-1"
        @mouseup=${this.onOptionClick}
        style=${this.portal ? 'display: none;' : ''}
        ${ref(this.hScrollElementRef)}
      >
        ${this.renderLoader()}
        ${this.renderSelectAll()}
        ${this.showNoResults && !this.optionsLoading && !this.loading
            ? this.renderEmptyState()
            : ComboboxRenderer.renderHorizontalGrid(virtualItems, totalSize, this.filteredData, this.value, this.multiple, this.gridRows, this.gridColumnWidth, this.getDisplayText.bind(this), this.getItemValue.bind(this), this.optionsLoading || this.loading, this.allowHtmlLabel, this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined, this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined, this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined)}
        ${showAddOption ? html `
          <div @mouseup=${(e) => { e.stopPropagation(); this.addCustomValue(this.searchValue.trim()); }}>
            ${ComboboxRenderer.renderAddCustomOption(this.searchValue.trim(), this.multiple)}
          </div>
        ` : ''}
        ${this.multiple && this.showFooter && !this.selectAllEnabled ? this.renderFooter() : ''}
      </div>
    `;
    }
    renderLoader() {
        if (this.loading) {
            return html `<span class="combobox__loader--center"><nile-loader size="sm"></nile-loader></span>`;
        }
        if (this.optionsLoading) {
            return html `<span class="combobox__loader"><nile-icon class="combobox__loader--icon" name="button-loading-blue"></nile-icon></span>`;
        }
        return html ``;
    }
    renderSelectAll() {
        if (!this.multiple || !this.selectAllEnabled)
            return html ``;
        const disabled = this.showNoResults || this.optionsLoading || this.loading;
        const anySelected = this.selectAllChecked || this.selectAllIndeterminate;
        const label = anySelected ? 'Deselect all' : 'Select all';
        const showToggleDisabled = this.selectedOptions.length === 0 || this.showNoResults;
        const iconColor = showToggleDisabled
            ? 'var(--nile-colors-primary-500, var(--ng-colors-fg-quaternary-400))'
            : 'var(--nile-colors-primary-600, var(--ng-colors-fg-brand-secondary-600))';
        return html `
      <div
        part="top-actions"
        class="combobox__top-actions ${disabled ? 'combobox__top-actions--disabled' : ''}"
        @mousedown=${(e) => e.preventDefault()}
      >
        <div
          class="combobox__select-all"
          @click=${this.handleSelectAllToggle}
        >
          <nile-checkbox
            ?checked=${this.selectAllChecked}
            ?indeterminate=${this.selectAllIndeterminate}
            ?disabled=${disabled}
          >
            ${label}
          </nile-checkbox>
        </div>

        <button
          part="show-toggle"
          class="combobox__show-toggle"
          type="button"
          ?disabled=${showToggleDisabled}
          @click=${this.toggleShowSelected}
        >
          <nile-icon
            library="system"
            name="filter"
            color=${iconColor}
          ></nile-icon>
          ${this.showSelectedOnly ? 'Show all' : 'Selected'}
        </button>
      </div>
    `;
    }
    renderFooter() {
        return html `
      <div
        part="footer"
        class="combobox__footer ${this.loading ? 'loading' : ''}"
        @click=${this.onFooterClick}
      >
        <span @click=${this.toggleShowSelected} style="cursor: pointer;">
          <nile-checkbox
            ?disabled=${this.selectedOptions.length === 0}
            ?checked=${this.showSelectedOnly}
          >
            &nbsp; Show Selected
          </nile-checkbox>
        </span>
        ${this.selectedOptions.length > 0
            ? html `<span class="combobox__footer-clear" @click=${this.clearAll}>Clear All</span>`
            : ''}
      </div>
    `;
    }
};
NileCombobox.styles = styles;
__decorate([
    query('.combobox-popup')
], NileCombobox.prototype, "popup", void 0);
__decorate([
    query('.combobox__trigger')
], NileCombobox.prototype, "combobox", void 0);
__decorate([
    query('.combobox__input')
], NileCombobox.prototype, "inputElement", void 0);
__decorate([
    query('.combobox__value-input')
], NileCombobox.prototype, "valueInput", void 0);
__decorate([
    state()
], NileCombobox.prototype, "hasFocus", void 0);
__decorate([
    state()
], NileCombobox.prototype, "displayLabel", void 0);
__decorate([
    state()
], NileCombobox.prototype, "selectedOptions", void 0);
__decorate([
    state()
], NileCombobox.prototype, "filteredData", void 0);
__decorate([
    state()
], NileCombobox.prototype, "filteredRows", void 0);
__decorate([
    state()
], NileCombobox.prototype, "originalData", void 0);
__decorate([
    state()
], NileCombobox.prototype, "showNoResults", void 0);
__decorate([
    state()
], NileCombobox.prototype, "showListbox", void 0);
__decorate([
    state()
], NileCombobox.prototype, "searchValue", void 0);
__decorate([
    state()
], NileCombobox.prototype, "showSelectedOnly", void 0);
__decorate([
    state()
], NileCombobox.prototype, "selectAllChecked", void 0);
__decorate([
    state()
], NileCombobox.prototype, "selectAllIndeterminate", void 0);
__decorate([
    state()
], NileCombobox.prototype, "stickyHeaderIndex", void 0);
__decorate([
    property()
], NileCombobox.prototype, "name", void 0);
__decorate([
    property({ type: Array })
], NileCombobox.prototype, "data", void 0);
__decorate([
    property({
        converter: {
            fromAttribute: (value) => value.split(' '),
            toAttribute: (value) => value.join(' '),
        },
    })
], NileCombobox.prototype, "value", void 0);
__decorate([
    defaultValue()
], NileCombobox.prototype, "defaultValue", void 0);
__decorate([
    property()
], NileCombobox.prototype, "size", void 0);
__decorate([
    property()
], NileCombobox.prototype, "placeholder", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "multiple", void 0);
__decorate([
    property()
], NileCombobox.prototype, "label", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "required", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "disabled", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "open", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "clearable", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "loading", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "optionsLoading", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "disableLocalSearch", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "allowCustomValue", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "acceptUserInput", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "addToSuggestions", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "strict", void 0);
__decorate([
    property({ attribute: 'max-tags-visible', type: Number })
], NileCombobox.prototype, "maxTagsVisible", void 0);
__decorate([
    property()
], NileCombobox.prototype, "tagLayout", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "showFooter", void 0);
__decorate([
    property({ type: Boolean, reflect: true, attribute: 'select-all-enabled' })
], NileCombobox.prototype, "selectAllEnabled", void 0);
__decorate([
    property({ type: Boolean, reflect: true, attribute: 'sticky-group-header' })
], NileCombobox.prototype, "stickyGroupHeader", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "portal", void 0);
__decorate([
    property({ type: Boolean })
], NileCombobox.prototype, "hoist", void 0);
__decorate([
    property({ reflect: true })
], NileCombobox.prototype, "placement", void 0);
__decorate([
    property({ reflect: true })
], NileCombobox.prototype, "form", void 0);
__decorate([
    property({ attribute: 'help-text', reflect: true })
], NileCombobox.prototype, "helpText", void 0);
__decorate([
    property({ attribute: 'error-message', reflect: true })
], NileCombobox.prototype, "errorMessage", void 0);
__decorate([
    property({ type: Boolean })
], NileCombobox.prototype, "warning", void 0);
__decorate([
    property({ type: Boolean })
], NileCombobox.prototype, "error", void 0);
__decorate([
    property({ type: Boolean })
], NileCombobox.prototype, "success", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "filled", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "pill", void 0);
__decorate([
    property({ type: String })
], NileCombobox.prototype, "noResultsMessage", void 0);
__decorate([
    property({ type: String, attribute: 'no-results-subtitle' })
], NileCombobox.prototype, "noResultsSubtitle", void 0);
__decorate([
    property({ type: String, attribute: 'no-data-message' })
], NileCombobox.prototype, "noDataMessage", void 0);
__decorate([
    property({
        type: Array,
        converter: {
            fromAttribute: (value) => {
                if (!value)
                    return [];
                try {
                    return JSON.parse(value);
                }
                catch {
                    return [];
                }
            },
            toAttribute: (value) => JSON.stringify(value),
        },
    })
], NileCombobox.prototype, "autoCompleteOptions", void 0);
__decorate([
    property({ type: Number })
], NileCombobox.prototype, "debounceMs", void 0);
__decorate([
    property({ attribute: false })
], NileCombobox.prototype, "renderItemConfig", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "allowHtmlLabel", void 0);
__decorate([
    property({ type: Boolean, reflect: true, attribute: true })
], NileCombobox.prototype, "enableVisibilityEffect", void 0);
__decorate([
    property({ type: Boolean, reflect: true, attribute: true })
], NileCombobox.prototype, "enableTabClose", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], NileCombobox.prototype, "noWidthSync", void 0);
__decorate([
    property({ attribute: 'grid-columns', type: Number, reflect: true })
], NileCombobox.prototype, "gridColumns", void 0);
__decorate([
    property({ attribute: 'grid-rows', type: Number, reflect: true })
], NileCombobox.prototype, "gridRows", void 0);
__decorate([
    property({ attribute: 'grid-column-width', type: Number })
], NileCombobox.prototype, "gridColumnWidth", void 0);
__decorate([
    watch('open', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleOpenChange", null);
__decorate([
    watch('disabled', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleDisabledChange", null);
__decorate([
    watch('value', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleValueChange", null);
__decorate([
    watch('data', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleDataChange", null);
__decorate([
    watch('autoCompleteOptions', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleAutoCompleteOptionsChange", null);
__decorate([
    watch('multiple', { waitUntilFirstUpdate: true }),
    watch('selectAllEnabled', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handleSelectAllConfigChange", null);
__decorate([
    watch('portal', { waitUntilFirstUpdate: true })
], NileCombobox.prototype, "handlePortalChange", null);
NileCombobox = __decorate([
    customElement('nile-combobox')
], NileCombobox);
export { NileCombobox };
// ── Default animations ──
setDefaultAnimation('combobox.show', {
    keyframes: [
        { opacity: 0, scale: 0.9 },
        { opacity: 1, scale: 1 },
    ],
    options: { duration: 100, easing: 'ease' },
});
setDefaultAnimation('combobox.hide', {
    keyframes: [
        { opacity: 1, scale: 1 },
        { opacity: 0, scale: 0.9 },
    ],
    options: { duration: 100, easing: 'ease' },
});
export default NileCombobox;
//# sourceMappingURL=nile-combobox.js.map