/**
 * `<Autocomplete.Root>` — Solid 2 port of Base UI React's
 * `AutocompleteRoot` (~280 LOC) layered on top of the combobox
 * primitives. Autocomplete is the "input value is the source of truth"
 * variant of combobox: the `value` prop controls the input text
 * directly, there's no separate selected value, and pressing an item
 * fills the input rather than committing a separate selection.
 *
 * ### Shipped scope
 *
 * - Single free-text autocomplete (`selectionMode: "none"`) under the
 *   hood — shares the combobox root-context shape so all combobox
 *   sub-parts (Portal, Positioner, Popup, List, Empty, Group,
 *   GroupLabel, ItemIndicator, Arrow, Clear) work via pass-through
 *   re-exports.
 * - Controlled + uncontrolled `value` (input text) and `open`.
 * - `mode` prop: `"list"` (default) / `"both"` / `"inline"` / `"none"`.
 *   `"list"` filters; `"none"` / `"inline"` disable filtering; `"both"`
 *   filters against the typed query even while inline-autocomplete
 *   updates the display value.
 * - `autoHighlight`, `openOnInputClick` (default `false`, unlike
 *   combobox where it defaults to `true`), `loopFocus`,
 *   `highlightItemOnHover`, `modal`, `keepHighlight` (prop accepted,
 *   hover-leave clearing mirrors combobox default).
 * - `onValueChange`, `onOpenChange`, `onOpenChangeComplete`,
 *   `onItemHighlighted` callbacks with `BaseUIChangeEventDetails`.
 * - Inline-autocomplete display-value composition: when the user
 *   keyboard-navigates over an item in `"inline"` / `"both"` modes, the
 *   input's visible value shows that item's label without updating the
 *   committed `value`.
 *
 * ### Deferred (inherits combobox's deferred sub-parts in shape)
 *
 * - Form-validation flow-through.
 * - `useClick` / `useDismiss` `enabled` latch.
 * - Positioning math in happy-dom.
 * - Input-inside-popup layout.
 * - Chips / multi-select / grid / virtualized (deferred sub-parts apply —
 *   autocomplete UX is single-input-only so these are less relevant).
 */

import { createEffect, createMemo, createSignal, createUniqueId, onCleanup, untrack } from "solid-js";
import { INITIAL_LAST_HIGHLIGHT } from "../../combobox/internals/constants.jsx";
import { getFilter } from "../../combobox/internals/filter.jsx";
import { defaultItemEquality, findItemIndex } from "../../combobox/internals/item-equality.jsx";
import { stringifyAsLabel } from "../../combobox/internals/resolve-value-label.jsx";
import { ComboboxRootContext, useComboboxRootContext } from "../../combobox/root/combobox-root-context.jsx";
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 { VISUALLY_HIDDEN_STYLE } from "../../internals/visually-hidden-style.jsx";
import { createAutocomplete } from "../create-autocomplete.jsx";
import { AutocompleteRootContext } from "./autocomplete-root-context.jsx";
// ---------------------------------------------------------------------------
// Inner — assumes <FloatingTree> is in scope.
// ---------------------------------------------------------------------------

function AutocompleteRootComponent(props) {
  // -------------------------------------------------------------------
  // Normalised props
  // -------------------------------------------------------------------
  const mode = () => props.mode ?? "list";
  const enableInline = () => {
    const m = mode();
    return m === "inline" || m === "both";
  };
  const 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 ?? false;
  const highlightItemOnHover = () => props.highlightItemOnHover ?? true;
  // Treat the itemToStringValue prop as the label source (matches React).
  const itemToStringLabel = () => props.itemToStringValue;
  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 signals
  // -------------------------------------------------------------------
  // Autocomplete has NO separate selection — the input value is it. We
  // mirror React's shape by keeping a `selectedValue` signal that's
  // always `null` (unused), so downstream combobox parts that read it
  // (Clear, etc.) stay correct.
  const [selectedValueSignal, setSelectedValueSignal] = createSignal(null, {
    ownedWrite: true
  });
  const selectedValue = () => selectedValueSignal();
  const setSelectedValue = next => {
    setSelectedValueSignal(() => next);
  };

  // Inline display value — the temporary input value derived from the
  // currently-highlighted item when `enableInline`. When non-empty, this
  // takes precedence over the user's typed value for display purposes.
  const [inlineInputValue, setInlineInputValue] = createSignal("", {
    ownedWrite: true
  });

  // Controlled / uncontrolled typed value (Base UI `value` / `defaultValue`
  // contract — autocomplete's `value` IS the input text): the `value` prop
  // wins while its resolved value is defined ("" IS controlled); otherwise
  // an internal signal (seeded once from `defaultValue`, mirroring React's
  // `useState(defaultValue ?? '')`) backs the state.
  const [typedValue, setUncontrolledValue] = createControlledState({
    controlled: () => readOption(props.value),
    default: () => readOption(props.defaultValue),
    fallback: "",
    name: "<Autocomplete.Root>",
    state: "value"
  });

  // Composed input value — inline display takes precedence when present.
  const inputValue = createMemo(() => {
    const inline = inlineInputValue();
    if (enableInline() && inline !== "") return inline;
    return typedValue();
  });

  // When the controlled value updates externally, drop any transient
  // inline value to avoid stale display. React gates this on
  // `isControlled` — uncontrolled writes clear the inline value inside
  // `setInputValue` instead.
  createEffect(() => ({
    v: readOption(props.value),
    controlled: readOption(props.value) !== undefined
  }), ({
    controlled
  }) => {
    if (controlled) setInlineInputValue("");
  });

  // Controlled / uncontrolled open state (Base UI `open` / `defaultOpen`
  // contract).
  const [open, setUncontrolledOpen] = createControlledState({
    controlled: () => {
      const controlled = readOption(props.open);
      return controlled === undefined ? undefined : Boolean(controlled);
    },
    default: () => readOption(props.defaultOpen),
    fallback: false,
    name: "<Autocomplete.Root>",
    state: "open"
  });
  const {
    mounted,
    transitionStatus,
    setMounted
  } = useTransitionStatus(open, () => true, () => false);

  // -------------------------------------------------------------------
  // Identity / element slots
  // -------------------------------------------------------------------
  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;
  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
  });
  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 chipsContainerRef = {
    current: null
  };
  const fieldControlRef = {
    current: null
  };
  const keyboardActiveRef = {
    current: true
  };
  const field = useFieldRootContext();

  // -------------------------------------------------------------------
  // Active / selected indices
  // -------------------------------------------------------------------
  const [activeIndex, setActiveIndex] = createSignal(null, {
    ownedWrite: true
  });
  const [selectedIndex, setSelectedIndex] = createSignal(null, {
    ownedWrite: true
  });
  const lastHighlightRef = {
    current: INITIAL_LAST_HIGHLIGHT
  };

  // Imperative highlight write. Called from event handlers AND from
  // effect-apply scopes (`useListNavigation` onNavigate, post-close
  // handleUnmount). Wrap in `untrack` so the inline-mode reads
  // (`enableInline()`, `itemToStringLabel()`) don't trip
  // `[STRICT_READ_UNTRACKED]`.
  const setIndices = options => untrack(() => {
    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) {
      // Clear inline display when highlight is cleared.
      if (enableInline()) {
        setInlineInputValue("");
      }
      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
      };
      // Inline-autocomplete: reflect the highlighted item's label in
      // the visible input — but only when highlight came from the
      // keyboard (pointer hover shouldn't overwrite typed text).
      if (enableInline() && type !== "pointer") {
        if (activeValue == null) {
          setInlineInputValue("");
        } else {
          setInlineInputValue(stringifyAsLabel(activeValue, itemToStringLabel()));
        }
      } else if (!enableInline()) {
        setInlineInputValue("");
      }
      props.onItemHighlighted?.(activeValue, createGenericEventDetails(type, undefined, {
        index: options.activeIndex
      }));
    }
  });

  // -------------------------------------------------------------------
  // Filtering — delegated to the createAutocomplete primitive.
  // The primitive owns items, isGrouped, filteredItems, flatFilteredItems,
  // and the query derivation. In "both" mode the primitive receives the
  // typed value (not the inline-overlay value) so the filter runs against
  // the actual user input, matching React's behaviour.
  // -------------------------------------------------------------------
  const _primitive = createAutocomplete({
    // In "both" mode filter against the typed value (not the inline overlay).
    value: () => mode() === "both" ? typedValue() : inputValue(),
    open,
    items: () => props.items,
    filter: () => props.filter,
    locale: () => props.locale,
    mode,
    itemToStringLabel,
    isItemEqualToValue: () => props.isItemEqualToValue ?? defaultItemEquality
  });
  const items = _primitive.items;
  const isGrouped = _primitive.isGrouped;
  const query = _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 `<Autocomplete.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. `mode: "inline" | "none"` are the static
  // modes (Base UI Autocomplete contract: items are never filtered), so
  // `matchesQuery` short-circuits to `true` there — every child stays
  // visible. Otherwise it uses the same default `Intl.Collator` "contains"
  // filter as items-mode (honoring a custom `filter` / `filter={null}`).
  const staticCollator = createMemo(() => getFilter({
    locale: props.locale
  }));
  const matchesQuery = value => {
    const m = mode();
    if (m === "inline" || m === "none") return true;
    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` synced with the filtered flat list when
  // `items` is supplied. The apply-side `itemToStringLabel()` read is a
  // snapshot — `untrack` so it's silent when the effect re-runs as part
  // of a parent flush.
  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 / handleSelection
  // -------------------------------------------------------------------
  // May be invoked from inside an effect-apply scope (e.g. floating-ui's
  // onOpenChange / onNavigate). Reads here are snapshots — wrap in
  // `untrack` to silence `[STRICT_READ_UNTRACKED]`.
  //
  // `setOpen` mirrors React's `AriaCombobox.setOpen` change flow: fire
  // `onOpenChange` FIRST, honor `details.cancel()`, then write the
  // uncontrolled state (a no-op for reads while the `open` prop is
  // controlled).
  const setOpen = (nextOpen, details) => untrack(() => {
    if (open() === nextOpen) return;
    props.onOpenChange?.(nextOpen, details);
    if (details.isCanceled) return;
    setUncontrolledOpen(nextOpen);
  });

  // Public input-value write. Mirrors React's `AutocompleteRoot`
  // `handleValueChange` EXACTLY: clear any transient inline value, write
  // the uncontrolled value, THEN fire `onValueChange`. Note Base UI writes
  // the internal value before the callback on this axis — `cancel()` does
  // not roll the typed value back (unlike `setOpen`).
  const setInputValue = (next, details) => untrack(() => {
    setInlineInputValue("");
    setUncontrolledValue(next);
    props.onValueChange?.(next, details);
  });

  // Sub-parts call this via root.setSelectedValue. For autocomplete,
  // "selecting" an item means filling the input with its label — routed
  // through `setInputValue` so the uncontrolled value updates too (React
  // routes this through `AriaCombobox.setInputValue` → `handleValueChange`).
  const setSelectedValueImpl = (next, details) => untrack(() => {
    const scalar = Array.isArray(next) ? null : next;
    setSelectedValue(scalar);
    if (details.reason === REASONS.itemPress || details.reason === REASONS.none || details.reason === REASONS.clearPress) {
      const labelValue = stringifyAsLabel(scalar, itemToStringLabel());
      setInputValue(labelValue, details);
    }
  });
  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);
    setSelectedValueImpl(itemValue, details);
    setOpen(false, details);
  });

  // -------------------------------------------------------------------
  // Post-close
  // -------------------------------------------------------------------
  // `handleUnmount` and the `onComplete` callback fire from
  // `useOpenChangeComplete`'s effect-apply scope. The `props.*` reads are
  // snapshots — wrap in `untrack` to silence
  // `[STRICT_READ_UNTRACKED]` on the reactive props proxy.
  const handleUnmount = () => untrack(() => {
    setMounted(false);
    setInlineInputValue("");
    props.onOpenChangeComplete?.(false);
    setIndices({
      activeIndex: null,
      selectedIndex: null
    });
  });
  useOpenChangeComplete({
    open,
    ref: popupRef,
    onComplete: () => untrack(() => {
      if (!open()) handleUnmount();else props.onOpenChangeComplete?.(true);
    })
  });

  // -------------------------------------------------------------------
  // Floating-UI
  // -------------------------------------------------------------------
  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
    });
  }));
  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"
    }
  }));
  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,
    // onNavigate runs in an effect-apply scope inside `useListNavigation`.
    // Wrap in `untrack` to silence `[STRICT_READ_UNTRACKED]` on the
    // `open()` / `transitionStatus()` snapshot reads.
    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. Capture the `query()` and
  // `activeIndex()` reads in compute (snapshot) so the apply scope —
  // which is unowned/strict-read — doesn't trip
  // `[STRICT_READ_UNTRACKED]`.
  //
  // The navigable item count is mode-dependent: in items-mode it's the
  // filtered flat list (`flatFilteredItems().length`); in static-children
  // mode (the cmdk pattern, no `items` prop) `flatFilteredItems` is ALWAYS
  // empty, so we count the live registered+visible `<Autocomplete.Item>`s
  // via `visibleItemCount()`. Using `flatFilteredItems` unconditionally here
  // was the static-children nav bug: with an empty flat list the effect
  // (a) short-circuited so `autoHighlight="always"` never highlighted item 0
  // and (b) — because `activeIndex()` is a dependency — re-ran on every
  // keyboard navigation and reset the freshly-set `activeIndex` back to
  // `null`, killing arrow-key movement entirely.
  createEffect(() => {
    const hasItems = items() !== undefined;
    return {
      typed: typedValue(),
      ahm: autoHighlightMode(),
      count: hasItems ? flatFilteredItems().length : visibleItemCount(),
      hasQuery: query() !== "",
      activeIdx: activeIndex()
    };
  }, ({
    ahm,
    count,
    hasQuery,
    activeIdx
  }) => untrack(() => {
    if (!ahm) return;
    if (count === 0) {
      setActiveIndex(null);
      return;
    }
    if (ahm === "always" || ahm === "input-change" && hasQuery) {
      if (activeIdx == null) setIndices({
        activeIndex: 0,
        type: "none"
      });
    }
  }));

  // -------------------------------------------------------------------
  // Field-context integration
  // -------------------------------------------------------------------
  const validation = useFieldControlValidation({
    value: inputValue,
    id,
    controlRef: fieldControlRef,
    getValue: () => inputValue()
  });
  createEffect(() => inputElement(), el => {
    fieldControlRef.current = el;
  });
  let initialFieldValue = untrack(inputValue);
  createEffect(() => inputValue(), v => untrack(() => {
    if (!field.present) return;
    if (v === initialFieldValue) return;
    initialFieldValue = v;
    const initial = field.validityData().initialValue;
    field.setDirty(v !== initial);
    if (field.shouldValidateOnChange()) {
      void validation.commit(v);
    } else {
      void validation.commit(v, true);
    }
  }));
  createEffect(() => inputValue(), v => untrack(() => {
    if (!field.present) return;
    field.setFilled(v !== "");
  }));

  // Imperative actions.
  const actions = {
    unmount: () => setMounted(false)
  };
  // `actionsRef` is a setup-time wiring contract (like React refs) — read
  // once, untracked, so it doesn't trip `[STRICT_READ_UNTRACKED]`.
  const ref = untrack(() => 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 (shape-compatible with Combobox so all combobox sub-parts
  // work via re-export).
  // -------------------------------------------------------------------
  const contextValue = {
    id,
    labelId,
    setLabelId,
    open,
    mounted,
    transitionStatus,
    inputValue,
    setInputValue,
    selectedValue,
    setSelectedValue: setSelectedValueImpl,
    selectionMode: () => "none",
    activeIndex,
    selectedIndex,
    setIndices,
    disabled,
    readOnly,
    required,
    autoHighlightMode,
    highlightItemOnHover,
    modal,
    loopFocus,
    openOnInputClick,
    // Autocomplete doesn't support grid/virtualized; provide inert accessors
    // for context-shape parity with combobox.
    grid: () => false,
    virtualized: () => false,
    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: () => {},
    floatingRootContext,
    interactions,
    keyboardActiveRef
  };

  // Sync selectedIndex when `selectedValue` changes (unused in
  // autocomplete but kept for context-shape parity with combobox).
  createEffect(() => ({
    sv: selectedValue(),
    list: flatItems()
  }), ({
    sv,
    list
  }) => untrack(() => {
    if (sv == null) {
      setSelectedIndex(null);
      return;
    }
    const registry = items() !== undefined ? list : allValuesRef.current;
    const idx = findItemIndex(registry, sv, isItemEqualToValue());
    setSelectedIndex(idx === -1 ? null : idx);
  }));
  const autocompleteContextValue = {
    mode
  };

  // -------------------------------------------------------------------
  // Browser-autofill hidden control input
  // -------------------------------------------------------------------
  // Mirrors React's `AriaCombobox` hidden text input: a visually-hidden,
  // tab-skipped, `aria-hidden` `<input type="text">` that receives native
  // browser-autofill events. When the browser writes a value into it via
  // autofill, we forward the value through `setInputValue` so the
  // autocomplete's root state (and the visible input) update in lockstep.
  // Focus delegation hands focus back to the visible input if an assistive
  // technology routes into the hidden control.
  const setHiddenInputRef = el => {
    validation.inputRef.current = el;
  };
  function handleHiddenAutofill(event) {
    // Autocomplete's selection model is `'none'` — the input value IS the
    // autocomplete value. Mirror React's autofill branch: treat the
    // autofilled value as a user-typed write and commit through the same
    // pipeline.
    const target = event.currentTarget;
    if (!target) return;
    const nextValue = target.value;
    const details = createChangeEventDetails(REASONS.none, event);
    setInputValue(nextValue, details);
  }
  function handleHiddenFocus() {
    const visible = inputElement() ?? triggerElement();
    visible?.focus();
  }

  // Fresh mutable copy — the base Solid style binding mutates its `prev`
  // object when reconciling, which would throw on a frozen / shared source.
  const hiddenStyle = () => ({
    ...VISUALLY_HIDDEN_STYLE
  });
  return <ComboboxRootContext value={contextValue}>
			<AutocompleteRootContext value={autocompleteContextValue}>
				{props.children}
				{/* biome-ignore lint/a11y/noAriaHiddenOnFocusable: tabindex={-1} is intentional — browsers focus this input during autofill, which we detect via onFocus/onInput. aria-hidden hides it from screen readers without blocking that native browser behavior. */}
				<input ref={setHiddenInputRef} type="text" id={typeof props.id === "string" ? `${props.id}-hidden-input` : undefined} tabindex={-1} aria-hidden="true" style={hiddenStyle()} disabled={disabled()} required={required()} readonly={readOnly()} value={inputValue()} onFocus={handleHiddenFocus} onInput={handleHiddenAutofill} onChange={handleHiddenAutofill} />
			</AutocompleteRootContext>
		</ComboboxRootContext>;
}

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