/**
 * `<Combobox.Root>` — Solid 2 port of Base UI React's `ComboboxRoot` +
 * `AriaCombobox` (~1600 LOC combined). The port scope is narrowed to the
 * single-select, list-autocomplete path that matches ~95% of combobox
 * usage; the deferred features are flagged in `remaining-work.md`.
 *
 * ### Shipped scope
 *
 * - Single-select + `selectionMode: "none"` (free-text autocomplete)
 * - Controlled + uncontrolled `value` / `inputValue` / `open`
 * - Synchronous client-side filtering via `items` + `Intl.Collator`
 * - Consumer-supplied `filter` function override
 * - `autoHighlight` ("input-change" / "always" modes)
 * - `loopFocus`, `openOnInputClick`, `highlightItemOnHover`, `modal`
 * - Keyboard navigation via `useListNavigation` (virtual focus,
 *   `aria-activedescendant`)
 * - `useTypeahead` integration on the list
 * - `useDismiss` (Escape + outside-press) and `useClick`
 * - `onOpenChange`, `onInputValueChange`, `onValueChange`,
 *   `onItemHighlighted` callbacks with `BaseUIChangeEventDetails`
 * - `onOpenChangeComplete` + `useTransitionStatus` exit-animation deferral
 * - `<Combobox.Portal>` + `<Combobox.Positioner>` + `<Combobox.Popup>` +
 *   `<Combobox.List>` + `<Combobox.Item>` + `<Combobox.ItemIndicator>` +
 *   `<Combobox.Empty>` + `<Combobox.Group>` + `<Combobox.Clear>` +
 *   `<Combobox.Trigger>` + `<Combobox.Input>` + `<Combobox.Arrow>`
 *
 * ### Deferred
 *
 * - **Multi-select mode** + `chips` / `chip` / `chip-remove` / `row` /
 *   `input-group` / `collection` / `backdrop` / `value` / `label` /
 *   `status` / `icon` sub-parts
 * - **Grid navigation** (`grid={true}` + `<Combobox.Row>` + `cols`/`orientation` on list-nav) — landed.
 * - **Virtualized lists** (`virtualized={true}`) — stable-index override landed.
 * - **Input-inside-popup layout** (the "select-shaped" combobox where the
 *   input is mounted inside the popup + `<Combobox.Trigger>` is the
 *   anchor)
 * - **Form integration** — full field-context write-through
 *   (`setDirty` / `setFilled` / `validation.commit`) is a no-op here;
 *   field registration is wired but validation does not flow.
 * - **`fillInputOnItemPress` / `submitOnItemClick`** — landed.
 * - **`inline` mode** (no popup — inline autocomplete within the input)
 * - **IME / composition handling** — the basic `change` handler is wired
 *   but the full Android/IME-composition path is not ported.
 * - **Live-region announcement** (`<Combobox.Empty>` renders the
 *   `role="status"` container but without the "initial text mutation" hack)
 * - **`closeQuery` / `queryChangedAfterOpen` machinery** — landed.
 * - **`allValuesRef` stable index tracking** — simplified: items
 *   registered via `<CompositeList>` only.
 *
 * Positioning math (happy-dom returns 0 rects) and full pointer flows
 * (focus restoration ordering, iOS touch-open delay) need real browser
 * coverage.
 */

import { createEffect, createMemo, createSignal, createUniqueId, onCleanup, untrack } from "solid-js";
import { createChangeEventDetails, createGenericEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { createControlledState } from "../../internals/create-controlled-state.jsx";
import { useFieldRootContext } from "../../internals/field-context.jsx";
import { FloatingTree, useFloatingParentNodeId } from "../../internals/floating-ui/components/floating-tree.jsx";
import { useClick } from "../../internals/floating-ui/hooks/use-click.jsx";
import { useDismiss } from "../../internals/floating-ui/hooks/use-dismiss.jsx";
import { useFloatingRootContext } from "../../internals/floating-ui/hooks/use-floating-root-context.jsx";
import { useInteractions } from "../../internals/floating-ui/hooks/use-interactions.jsx";
import { useListNavigation } from "../../internals/floating-ui/hooks/use-list-navigation.jsx";
import { useRole } from "../../internals/floating-ui/hooks/use-role.jsx";
import { readOption } from "../../internals/maybe-accessor.jsx";
import { useOpenChangeComplete } from "../../internals/popups/use-open-change-complete.jsx";
import { useFieldControlValidation } from "../../internals/use-field-control-validation/use-field-control-validation.jsx";
import { useTransitionStatus } from "../../internals/use-transition-status/use-transition-status.jsx";
import { createCombobox } from "../create-combobox.jsx";
import { INITIAL_LAST_HIGHLIGHT } from "../internals/constants.jsx";
import { getFilter } from "../internals/filter.jsx";
import { defaultItemEquality, findItemIndex, removeItem, selectedValueIncludes } from "../internals/item-equality.jsx";
import { stringifyAsLabel, stringifyAsValue } from "../internals/resolve-value-label.jsx";
import { ComboboxRootContext, useComboboxRootContext } from "./combobox-root-context.jsx";
// -----------------------------------------------------------------------
// Inner implementation — assumes `<FloatingTree>` is in scope.
// -----------------------------------------------------------------------

function ComboboxRootComponent(props) {
  // -------------------------------------------------------------------
  // Normalised props
  // -------------------------------------------------------------------
  // `multiple` wins when explicitly set; otherwise fall back to
  // `selectionMode`. This mirrors React's `ComboboxRoot` wrapper which
  // converts `multiple={true}` → `selectionMode="multiple"` and leaves
  // `selectionMode` in place otherwise.
  const selectionMode = () => {
    if (props.multiple === true) return "multiple";
    return props.selectionMode ?? "single";
  };
  const single = () => selectionMode() === "single";
  const multiple = () => selectionMode() === "multiple";
  const field = useFieldRootContext();
  const fieldName = () => field.name() ?? props.name;
  const disabled = () => field.disabled() || (props.disabled ?? false);
  const readOnly = () => props.readOnly ?? false;
  const required = () => props.required ?? false;
  const modal = () => props.modal ?? false;
  const loopFocus = () => props.loopFocus ?? true;
  const openOnInputClick = () => props.openOnInputClick ?? true;
  const highlightItemOnHover = () => props.highlightItemOnHover ?? true;
  const grid = () => props.grid ?? false;
  const virtualized = () => props.virtualized ?? false;
  const submitOnItemClick = () => props.submitOnItemClick ?? false;
  const fillInputOnItemPress = () => props.fillInputOnItemPress ?? true;
  const itemToStringLabel = () => props.itemToStringLabel;
  const itemToStringValue = () => props.itemToStringValue;
  const isItemEqualToValue = () => props.isItemEqualToValue ?? defaultItemEquality;
  const autoHighlightMode = () => {
    const v = props.autoHighlight;
    if (v === "always") return "always";
    return v ? "input-change" : false;
  };

  // -------------------------------------------------------------------
  // Controlled / uncontrolled state (Base UI `useControlled` contract,
  // applied to all THREE axes like React's `AriaCombobox`):
  // - `value` is controlled while its resolved value is not `undefined` —
  //   `value={null}` IS controlled. Uncontrolled fallback is `null`
  //   (`[]` in multi-select, matching React's
  //   `multiple ? defaultSelectedValue ?? EMPTY_ARRAY : defaultSelectedValue`).
  // - `inputValue` is controlled while not `undefined` ("" IS controlled).
  //   The uncontrolled seed mirrors React's `initialDefaultInputValue`:
  //   `defaultInputValue`, else the initial selected value's label in
  //   single mode, else "".
  // - `open` is controlled while not `undefined`, seeded from `defaultOpen`.
  // -------------------------------------------------------------------
  // Freeze the multi-select latch at setup — the selection-mode decision
  // cannot flip after mount (the scalar vs. array value shape would
  // diverge from the consumer). This mirrors React's controlled-vs-uncontrolled
  // branch freezing (see solidify.md §6).
  const isMultipleAtInit = untrack(() => props.multiple === true || props.selectionMode === "multiple");
  const emptyArray = [];
  const [selectedValue, setUncontrolledSelectedValue] = createControlledState({
    controlled: () => readOption(props.value),
    default: () => readOption(props.defaultValue),
    fallback: isMultipleAtInit ? emptyArray : null,
    name: "<Combobox.Root>",
    state: "value"
  });

  // Whether the consumer engaged the inputValue axis at all (controlled
  // prop OR default). When neither is supplied, the input display follows
  // the selection in single mode (seed below + the selectedValue→input
  // sync effect), mirroring React's `hasInputValue`.
  const hasInputValueProp = () => readOption(props.inputValue) !== undefined || readOption(props.defaultInputValue) !== undefined;
  const [inputValue, setUncontrolledInputValue] = createControlledState({
    controlled: () => readOption(props.inputValue),
    default: () => readOption(props.defaultInputValue),
    // React's `initialDefaultInputValue`: when neither `inputValue` nor
    // `defaultInputValue` is provided, derive the seed from the initial
    // selected value's label in single mode so the input reflects the
    // selection on mount.
    fallback: untrack(() => {
      if (hasInputValueProp()) return "";
      if (single()) {
        const sv = selectedValue();
        if (sv === null || Array.isArray(sv)) return "";
        return stringifyAsLabel(sv, itemToStringLabel());
      }
      return "";
    }),
    name: "<Combobox.Root>",
    state: "inputValue"
  });
  const [open, setUncontrolledOpen] = createControlledState({
    controlled: () => {
      const controlled = readOption(props.open);
      return controlled === undefined ? undefined : Boolean(controlled);
    },
    default: () => readOption(props.defaultOpen),
    fallback: false,
    name: "<Combobox.Root>",
    state: "open"
  });

  // React's "mid-close filter freeze": when the popup is closing we snapshot
  // the last query into `closeQuery`, and the `query` memo below prefers the
  // snapshot while `closeQuery !== null`. Cleared on unmount (post-close) or
  // when a fresh open transitions false→true. `queryChangedAfterOpen` is set
  // once the user has typed anything after opening so the freeze only
  // activates when there's an actual filtered view to keep stable.
  const [closeQuery, setCloseQuery] = createSignal(null, {
    ownedWrite: true
  });
  const [queryChangedAfterOpen, setQueryChangedAfterOpen] = createSignal(false, {
    ownedWrite: true
  });

  // Mount / transition machinery.
  const {
    mounted,
    transitionStatus,
    setMounted
  } = useTransitionStatus(open, () => true, () => false);

  // Identity
  const [labelId, setLabelId] = createSignal(undefined, {
    ownedWrite: true
  });
  // Base UI's React root auto-generates a stable `useId()` when the consumer
  // passes no `id`. Without that, item ids (`${root.id()}-${index}`) collapse
  // to the literal `"undefined-0"` and the input's `aria-activedescendant`
  // points at a nonexistent node — virtual focus / arrow-nav into the list
  // silently breaks (e.g. the gitcafe Command palette, which supplies no id).
  // Mirror that here with a `createUniqueId()` fallback, stable for the
  // lifetime of the root, only used when the consumer omits `id`.
  const fallbackId = createUniqueId();
  const id = () => typeof props.id === "string" ? props.id : fallbackId;

  // -------------------------------------------------------------------
  // Element slots
  // -------------------------------------------------------------------
  const [triggerElement, setTriggerElementInternal] = createSignal(null, {
    ownedWrite: true
  });
  const [inputElement, setInputElementInternal] = createSignal(null, {
    ownedWrite: true
  });
  const [listElement, setListElementInternal] = createSignal(null, {
    ownedWrite: true
  });
  const [positionerElement, setPositionerElementInternal] = createSignal(null, {
    ownedWrite: true
  });
  const [popupSide, setPopupSide] = createSignal(null, {
    ownedWrite: true
  });

  // Refs
  const listRef = {
    current: []
  };
  const labelsRef = {
    current: []
  };
  const popupRef = {
    current: null
  };
  const inputRef = {
    current: null
  };
  const emptyRef = {
    current: null
  };
  const clearRef = {
    current: null
  };
  const valuesRef = {
    current: []
  };
  const allValuesRef = {
    current: []
  };
  const keyboardActiveRef = {
    current: true
  };
  const chipsContainerRef = {
    current: null
  };

  // -------------------------------------------------------------------
  // Active / selected indices
  // -------------------------------------------------------------------
  const [activeIndex, setActiveIndex] = createSignal(null, {
    ownedWrite: true
  });
  const [selectedIndex, setSelectedIndex] = createSignal(null, {
    ownedWrite: true
  });
  const lastHighlightRef = {
    current: INITIAL_LAST_HIGHLIGHT
  };
  const setIndices = options => {
    if (options.activeIndex !== undefined) {
      setActiveIndex(options.activeIndex);
    }
    if (options.selectedIndex !== undefined) {
      setSelectedIndex(options.selectedIndex);
    }
    const type = options.type ?? "none";
    if (options.activeIndex === undefined) return;
    if (options.activeIndex === null) {
      if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
        lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
        props.onItemHighlighted?.(undefined, createGenericEventDetails(type, undefined, {
          index: -1
        }));
      }
    } else {
      const activeValue = valuesRef.current[options.activeIndex];
      lastHighlightRef.current = {
        value: activeValue,
        index: options.activeIndex
      };
      props.onItemHighlighted?.(activeValue, createGenericEventDetails(type, undefined, {
        index: options.activeIndex
      }));
    }
  };

  // -------------------------------------------------------------------
  // Filtering — delegated to the createCombobox primitive.
  // The primitive owns items, isGrouped, filteredItems, flatFilteredItems,
  // and the base query derivation. The wrapper layers the mid-close
  // "closeQuery" freeze on top of the primitive's query so the popup's
  // filtered view stays stable while the exit animation plays.
  // -------------------------------------------------------------------
  const _primitive = createCombobox({
    value: selectedValue,
    inputValue: () => {
      // Pass closeQuery as a frozen inputValue when in the closing freeze,
      // so the primitive's query (and filteredItems) use the frozen string.
      const frozen = closeQuery();
      if (frozen !== null) return frozen;
      return inputValue();
    },
    open,
    items: () => props.items,
    filter: () => props.filter,
    locale: () => props.locale,
    queryChangedAfterOpen: () => queryChangedAfterOpen(),
    selectionMode,
    itemToStringLabel,
    itemToStringValue,
    isItemEqualToValue: () => props.isItemEqualToValue ?? defaultItemEquality
  });
  const items = _primitive.items;
  const isGrouped = _primitive.isGrouped;
  // query: prefer the close-query freeze; otherwise use the primitive's query.
  const query = createMemo(() => {
    const frozen = closeQuery();
    if (frozen !== null) return frozen;
    return _primitive.query();
  });
  const filteredItems = _primitive.filteredItems;
  const flatFilteredItems = _primitive.flatFilteredItems;
  const flatItems = createMemo(() => {
    const list = items();
    if (!list) return [];
    if (isGrouped()) {
      return list.flatMap(g => g.items);
    }
    return list;
  });

  // -------------------------------------------------------------------
  // Static-children filtering (no `items` prop — the cmdk pattern)
  // -------------------------------------------------------------------
  // When `items` is omitted the consumer renders `<Combobox.Item>` children
  // directly, so there is no `flatFilteredItems` to drive empty-state /
  // per-item visibility. We filter the registered children against the live
  // query here using the same default `Intl.Collator` "contains" filter as
  // items-mode (honoring a custom `filter` / `filter={null}`). Each
  // `<Combobox.Item>` reports its visibility via `setItemVisible`;
  // `visibleItemCount` is the reactive total `<Combobox.Empty>` /
  // `<Combobox.List>` read. Inert in items-mode.
  const staticCollator = createMemo(() => getFilter({
    locale: props.locale
  }));
  const matchesQuery = value => {
    const q = query().trim();
    if (q === "") return true;
    const userFilter = props.filter;
    if (userFilter === null) return true;
    if (userFilter !== undefined) {
      return userFilter(value, q, itemToStringLabel());
    }
    return staticCollator().contains(value, q, itemToStringLabel());
  };
  const visibleItemTokens = new Set();
  const [visibleItemTick, setVisibleItemTick] = createSignal(0, {
    ownedWrite: true
  });
  const setItemVisible = (token, visible) => {
    const had = visibleItemTokens.has(token);
    if (visible === had) return;
    if (visible) visibleItemTokens.add(token);else visibleItemTokens.delete(token);
    setVisibleItemTick(t => t + 1);
  };
  const visibleItemCount = createMemo(() => {
    visibleItemTick();
    return visibleItemTokens.size;
  });

  // Keep `valuesRef` / `listRef` in sync with the filtered flat list when
  // `items` is supplied. When `items` is omitted, the <CompositeList>-driven
  // item registration path owns these.
  createEffect(() => ({
    hasItems: items() !== undefined,
    flat: flatFilteredItems()
  }), ({
    hasItems,
    flat
  }) => untrack(() => {
    if (!hasItems) return;
    valuesRef.current = flat;
    listRef.current.length = flat.length;
    labelsRef.current = flat.map(item => stringifyAsLabel(item, itemToStringLabel()));
  }));

  // -------------------------------------------------------------------
  // setOpen / setInputValue / setSelectedValue — the core write path.
  // -------------------------------------------------------------------
  // Imperative write-paths. These may be invoked from inside an effect's
  // apply scope (e.g. floating-ui's onOpenChange callback fired from
  // `useDismiss` / `useClick` apply, or onNavigate fired from
  // `useListNavigation`). The reads below are snapshots — there's no
  // subscription contract here — so wrap the body in `untrack` to keep
  // `[STRICT_READ_UNTRACKED]` quiet while preserving semantics.
  // Each setter mirrors React's `AriaCombobox` change flow exactly: fire
  // the `onXChange` callback FIRST, honor `details.cancel()`, THEN write
  // the uncontrolled state (a no-op for reads while the matching prop is
  // controlled — the consumer's signal write is the source of truth) and
  // run the downstream bookkeeping.
  const setOpen = (nextOpen, details) => untrack(() => {
    if (open() === nextOpen) return;
    props.onOpenChange?.(nextOpen, details);
    if (details.isCanceled) return;
    if (!nextOpen && queryChangedAfterOpen()) {
      // Snapshot the current typed query so the popup's filter view stays
      // stable while the exit transition plays. Cleared in handleUnmount.
      const q = inputValue().trim();
      setCloseQuery(q);
      if (q === "") setQueryChangedAfterOpen(false);
    }
    if (nextOpen) {
      setCloseQuery(null);
      setQueryChangedAfterOpen(false);
    }
    setUncontrolledOpen(nextOpen);
  });
  const setInputValue = (next, details) => untrack(() => {
    props.onInputValueChange?.(next, details);
    if (details.isCanceled) return;
    // Typed input after open marks the filter as dirty so the mid-close
    // freeze can latch on close. Non-typed sources (programmatic resets,
    // item-press fill) are exempt.
    if (details.reason === REASONS.inputChange && open() && next.trim() !== "") {
      setQueryChangedAfterOpen(true);
    }
    setUncontrolledInputValue(next);
  });
  const setSelectedValue = (next, details) => untrack(() => {
    const normalized = next ?? (isMultipleAtInit ? emptyArray : null);
    props.onValueChange?.(normalized, details);
    if (details.isCanceled) return;
    setUncontrolledSelectedValue(normalized);
    // In single mode, sync the input to the selected label (unless the
    // caller is currently mid-type). React gates this behind the same
    // cancel check — a canceled selection leaves the input untouched.
    if (single() && !Array.isArray(normalized) && (details.reason === REASONS.itemPress || details.reason === REASONS.none || details.reason === REASONS.clearPress)) {
      setInputValue(stringifyAsLabel(normalized, itemToStringLabel()), createChangeEventDetails(details.reason, details.event));
    }
  });
  const handleSelection = (event, passedValue) => untrack(() => {
    let itemValue = passedValue;
    if (itemValue === undefined) {
      const ai = activeIndex();
      if (ai === null) return;
      itemValue = valuesRef.current[ai];
    }
    const details = createChangeEventDetails(REASONS.itemPress, event);
    if (multiple()) {
      const current = selectedValue();
      const currentArr = Array.isArray(current) ? current : [];
      const isSelected = selectedValueIncludes(currentArr, itemValue, isItemEqualToValue());
      const nextArr = isSelected ? removeItem(currentArr, itemValue, isItemEqualToValue()) : [...currentArr, itemValue];
      setSelectedValue(nextArr, details);
      // Clear the typed filter if the user was filtering — next open
      // starts fresh. Keep the popup open so multiple items can be
      // toggled.
      const wasFiltering = inputRef.current ? inputRef.current.value.trim() !== "" : false;
      if (wasFiltering) {
        setOpen(false, details);
      }
      return;
    }
    if (selectionMode() === "none") {
      // Free-text mode: items just project their label into the input;
      // the selected-value state is untouched unless fillInputOnItemPress.
      if (fillInputOnItemPress()) {
        setInputValue(stringifyAsLabel(itemValue, itemToStringLabel()), details);
      }
      setOpen(false, details);
      if (submitOnItemClick()) {
        const formEl = inputRef.current?.form ?? inputRef.current?.closest("form");
        if (formEl && typeof formEl.requestSubmit === "function") {
          formEl.requestSubmit();
        }
      }
      return;
    }
    setSelectedValue(itemValue, details);
    setOpen(false, details);
  });

  // -------------------------------------------------------------------
  // Post-close side effects
  // -------------------------------------------------------------------
  const handleUnmount = () => {
    setMounted(false);
    props.onOpenChangeComplete?.(false);
    setCloseQuery(null);
    setQueryChangedAfterOpen(false);
    setIndices({
      activeIndex: null,
      selectedIndex: null
    });
    // Multi-select: clear the typed filter after close so the next open
    // starts fresh. Single: sync the input to the current selected label.
    if (multiple()) {
      if (inputRef.current && inputRef.current.value !== "") {
        setInputValue("", createChangeEventDetails(REASONS.inputClear, undefined));
      }
      return;
    }
    if (single()) {
      const sv = selectedValue();
      const stringVal = stringifyAsLabel(Array.isArray(sv) ? null : sv, itemToStringLabel());
      if (inputRef.current && inputRef.current.value !== stringVal) {
        // React uses `inputClear` when no selection was made (treats the
        // close as clearing the typed filter), `none` otherwise.
        setInputValue(stringVal, createChangeEventDetails(stringVal === "" ? REASONS.inputClear : REASONS.none, undefined));
      }
    }
  };
  useOpenChangeComplete({
    open,
    ref: popupRef,
    onComplete: () => {
      if (!open()) handleUnmount();else props.onOpenChangeComplete?.(true);
    }
  });

  // Sync selectedIndex when `selectedValue` changes. Capture all reactive
  // inputs in compute (snapshot pattern). The apply runs in an
  // unowned/untracked scope — wrap in `untrack` so any nested signal reads
  // stay quiet.
  createEffect(() => ({
    sv: selectedValue(),
    list: flatItems(),
    hasItems: items() !== undefined,
    comparer: isItemEqualToValue()
  }), snap => untrack(() => {
    const {
      sv,
      list,
      hasItems,
      comparer
    } = snap;
    if (sv == null) {
      setSelectedIndex(null);
      return;
    }
    const registry = hasItems ? list : allValuesRef.current;
    if (Array.isArray(sv)) {
      const arr = sv;
      if (arr.length === 0) {
        setSelectedIndex(null);
        return;
      }
      const last = arr[arr.length - 1];
      const idx = findItemIndex(registry, last, comparer);
      setSelectedIndex(idx === -1 ? null : idx);
      return;
    }
    const idx = findItemIndex(registry, sv, comparer);
    setSelectedIndex(idx === -1 ? null : idx);
  }));

  // Mirror React's `useValueChanged(selectedValue, …)` input sync: when the
  // consumer engages neither `inputValue` nor `defaultInputValue`, the
  // visible input follows the selection in single mode — external `value`
  // writes update the display text (this replaces the old reactive
  // "computed default" memo; selection-by-item-press is already handled
  // inside `setSelectedValue`).
  let prevSelectedForInputSync = untrack(selectedValue);
  createEffect(() => selectedValue(), sv => untrack(() => {
    if (Object.is(sv, prevSelectedForInputSync)) return;
    prevSelectedForInputSync = sv;
    if (!single()) return;
    if (hasInputValueProp()) return;
    const nextInputValue = stringifyAsLabel(Array.isArray(sv) ? null : sv, itemToStringLabel());
    if (inputValue() !== nextInputValue) {
      setInputValue(nextInputValue, createChangeEventDetails(REASONS.none, undefined));
    }
  }));

  // -------------------------------------------------------------------
  // Field-context integration
  // -------------------------------------------------------------------
  // The value that a `<Field.Root>` should validate against.
  // - selectionMode "none": the input string
  // - selectionMode "single": the selected scalar value
  // - selectionMode "multiple": the selected array
  const fieldRawValue = createMemo(() => {
    if (selectionMode() === "none") return inputValue();
    return selectedValue();
  });
  // Projected to a string / string[] for form submission.
  const fieldStringValue = createMemo(() => {
    const mode = selectionMode();
    const raw = fieldRawValue();
    if (mode === "none") return raw;
    if (Array.isArray(raw)) {
      const itv = itemToStringValue();
      return raw.map(v => stringifyAsValue(v, itv));
    }
    const itv = itemToStringValue();
    return stringifyAsValue(raw, itv);
  });
  const fieldControlRef = {
    current: null
  };
  const validation = useFieldControlValidation({
    value: fieldRawValue,
    id,
    controlRef: fieldControlRef,
    getValue: () => fieldStringValue()
  });
  // Keep `fieldControlRef` synced to the live input element so the
  // registration carries a real control reference.
  createEffect(() => inputElement(), el => {
    fieldControlRef.current = el;
  });
  // Mirror React's `useValueChanged(selectedValue, …)` / `useValueChanged(inputValue, …)`
  // — push `setDirty` + run validation on each change.
  let initialDirtyValue = untrack(fieldRawValue);
  createEffect(() => fieldRawValue(), v => untrack(() => {
    if (!field.present) return;
    // Avoid firing on the initial mount cycle (no value change yet).
    if (Object.is(v, initialDirtyValue)) return;
    initialDirtyValue = v;
    const initial = field.validityData().initialValue;
    field.setDirty(v !== initial);
    if (field.shouldValidateOnChange()) {
      void validation.commit(v);
    } else {
      void validation.commit(v, true);
    }
  }));
  // `setFilled` — reflects whether the control has a user-visible value.
  createEffect(() => ({
    mode: selectionMode(),
    iv: inputValue(),
    sv: selectedValue()
  }), ({
    mode,
    iv,
    sv
  }) => untrack(() => {
    if (!field.present) return;
    if (mode === "none") {
      field.setFilled(iv !== "");
      return;
    }
    if (Array.isArray(sv)) {
      field.setFilled(sv.length > 0);
      return;
    }
    field.setFilled(sv != null);
  }));

  // -------------------------------------------------------------------
  // Floating-UI integration
  // -------------------------------------------------------------------
  const floatingRootContext = useFloatingRootContext({
    open: untrack(open),
    onOpenChange: (nextOpen, details) => {
      setOpen(nextOpen, details);
    }
  });
  createEffect(() => ({
    openValue: open(),
    reference: inputElement() ?? triggerElement(),
    floating: positionerElement(),
    status: transitionStatus()
  }), ({
    openValue,
    reference,
    floating,
    status
  }) => untrack(() => {
    floatingRootContext.update({
      open: openValue,
      referenceElement: reference,
      domReferenceElement: reference,
      floatingElement: floating,
      transitionStatus: status
    });
  }));

  // Floating-UI interaction hooks read accessor inputs once at body
  // time to seed local state — wrap in `untrack` so the component-body
  // strict-read shell stays quiet. Reactivity flows through the
  // accessor closures the hooks retain for subsequent updates.
  const role = untrack(() => useRole(floatingRootContext, {
    role: "listbox"
  }));
  const click = untrack(() => useClick(floatingRootContext, {
    enabled: () => !readOnly() && !disabled() && openOnInputClick(),
    event: "mousedown-only",
    toggle: false
  }));
  const dismiss = untrack(() => useDismiss(floatingRootContext, {
    enabled: () => !readOnly() && !disabled(),
    outsidePressEvent: {
      mouse: "sloppy",
      touch: "intentional"
    },
    outsidePress: event => {
      // Don't dismiss when clicking inside the chips container (e.g. on
      // a `<Combobox.ChipRemove>` button).
      const target = event.target;
      const chipsEl = chipsContainerRef.current;
      if (chipsEl && target && chipsEl.contains(target)) return false;
      return true;
    }
  }));
  const listNavigation = untrack(() => useListNavigation(floatingRootContext, {
    enabled: () => !readOnly() && !disabled(),
    id,
    listRef,
    activeIndex,
    selectedIndex,
    virtual: true,
    loopFocus,
    allowEscape: () => loopFocus() && !autoHighlightMode(),
    focusItemOnOpen: () => autoHighlightMode() ? "auto" : false,
    focusItemOnHover: highlightItemOnHover,
    resetOnPointerLeave: true,
    cols: () => grid() ? 2 : 1,
    orientation: () => grid() ? "horizontal" : "vertical",
    // onNavigate is invoked from `useListNavigation`'s effect-apply scope.
    // `open()` / `transitionStatus()` reads are snapshots, not subscriptions,
    // so wrap in `untrack` to silence `[STRICT_READ_UNTRACKED]`.
    onNavigate: (nextActiveIndex, event) => untrack(() => {
      if (!event && !open()) return;
      if (transitionStatus() === "ending") return;
      setIndices({
        activeIndex: nextActiveIndex,
        type: event ? keyboardActiveRef.current ? "keyboard" : "pointer" : "none"
      });
    })
  }));
  const interactions = untrack(() => useInteractions([role, click, dismiss, listNavigation]));

  // Auto-highlight on input change.
  createEffect(() => ({
    iv: inputValue(),
    mode: autoHighlightMode(),
    ff: flatFilteredItems(),
    q: query(),
    ai: activeIndex()
  }), ({
    mode,
    ff,
    q,
    ai
  }) => untrack(() => {
    if (!mode) return;
    if (ff.length === 0) {
      setActiveIndex(null);
      return;
    }
    if (mode === "always" || mode === "input-change" && q !== "") {
      if (ai == null) setActiveIndex(0);
    }
  }));

  // Virtualized stable-index override: when the consumer renders a windowed
  // subset of `listRef`, the sliced array mutates as the viewport scrolls
  // and the active index would otherwise be clamped to the visible range.
  // React's active-index effect skips the `>= candidateItems.length` reset
  // when `virtualized` is on so the logical index is preserved across
  // slice shifts. Mirror that here by not clamping when virtualized.
  createEffect(() => ({
    virt: virtualized(),
    ai: activeIndex(),
    len: flatFilteredItems().length,
    openV: open()
  }), ({
    virt,
    ai,
    len,
    openV
  }) => {
    if (!openV) return;
    if (ai == null) return;
    if (virt) return;
    if (ai >= len) {
      setActiveIndex(null);
    }
  });

  // Imperative actions.
  const actions = {
    unmount: () => setMounted(false)
  };
  const ref = props.actionsRef;
  if (typeof ref === "function") {
    ref(actions);
    onCleanup(() => ref(null));
  } else if (ref && typeof ref === "object") {
    ref.current = actions;
    onCleanup(() => {
      ref.current = null;
    });
  }

  // -------------------------------------------------------------------
  // Context value
  // -------------------------------------------------------------------
  const contextValue = {
    id,
    labelId,
    setLabelId,
    open,
    mounted,
    transitionStatus,
    inputValue,
    setInputValue,
    selectedValue,
    setSelectedValue,
    selectionMode,
    activeIndex,
    selectedIndex,
    setIndices,
    disabled,
    readOnly,
    required,
    autoHighlightMode,
    highlightItemOnHover,
    modal,
    loopFocus,
    openOnInputClick,
    grid,
    virtualized,
    itemToStringLabel,
    itemToStringValue,
    isItemEqualToValue,
    items,
    filteredItems,
    flatFilteredItems,
    query,
    matchesQuery,
    setItemVisible,
    visibleItemCount,
    listRef,
    labelsRef,
    popupRef,
    inputRef,
    emptyRef,
    clearRef,
    valuesRef,
    allValuesRef,
    chipsContainerRef,
    triggerElement,
    setTriggerElement: setTriggerElementInternal,
    inputElement,
    setInputElement: el => {
      inputRef.current = el;
      setInputElementInternal(el);
    },
    listElement,
    setListElement: setListElementInternal,
    positionerElement,
    setPositionerElement: setPositionerElementInternal,
    popupSide,
    setPopupSide,
    setOpen,
    handleSelection,
    forceMount: () => {
      // No-op for now. In React this is used to pre-populate the labels
      // ref from `items`; our port does that eagerly in the
      // `items` / `flatFilteredItems` effect above.
    },
    floatingRootContext,
    interactions,
    keyboardActiveRef
  };
  return <ComboboxRootContext value={contextValue}>
			{props.children}
			{multiple() && fieldName() ? (() => {
      const sv = selectedValue();
      if (!Array.isArray(sv)) return null;
      const itv = itemToStringValue();
      const n = fieldName();
      return sv.map(v => <input type="hidden" name={n} form={props.form} value={stringifyAsValue(v, itv)} />);
    })() : null}
		</ComboboxRootContext>;
}

/**
 * Groups all parts of the combobox. Doesn't render its own HTML element.
 */
export function ComboboxRoot(props) {
  if (useComboboxRootContext(true) != null || useFloatingParentNodeId() != null) {
    return <ComboboxRootComponent {...props} />;
  }
  return <FloatingTree>
			<ComboboxRootComponent {...props} />
		</FloatingTree>;
}
//# sourceMappingURL=combobox-root.jsx.map
