/**
 * Stage 4.5 port of Base UI React's `useListNavigation` hook.
 *
 * React source:
 *   `base-ui/packages/react/src/floating-ui-react/hooks/useListNavigation.ts`
 *
 * Adds arrow-key navigation (1D + 2D grid), Home/End, typeable-combobox
 * integration, virtual focus (`aria-activedescendant`), and tree-aware
 * submenu open/close behavior. This is the single biggest interaction
 * hook in the fork — every listbox / menu / menubar / combobox-grid
 * consumer in tier-3a reaches for it.
 *
 * ### React → Solid deviations
 *
 * 1. `useRef` → `MutableRef<T>` (matches sibling hooks). `useValueAsRef`
 *    (React fork helper that mirrors a prop into a ref) is replaced by
 *    reading the prop directly inside handlers/effects — Solid props
 *    are stable references for the lifetime of the component body, and
 *    callers pass reactive accessors when live updates matter (see the
 *    `MaybeAccessor` wrappers on `activeIndex`, `selectedIndex`,
 *    `disabledIndices`, etc.).
 *
 * 2. `useStableCallback` → `createStableCallback` from
 *    `src/internals/stable-callback.ts`. Used only where the React
 *    source stabilized identity for effect deps — Solid effects don't
 *    re-fire on callback identity so most handlers are plain functions.
 *
 * 3. `useIsoLayoutEffect` (with React single-callback + deps) →
 *    `createRenderEffect`-backed `useIsoLayoutEffect(compute, apply)`
 *    in split form. Tracked reads go in the `compute` factory; the
 *    side-effect body runs in `apply`.
 *
 * 4. `useMemo` for `item` / `floating` / `trigger` / `reference` bags
 *    collapses to plain literals. Solid doesn't re-run component bodies,
 *    so the bag identity is already stable for the lifetime of the call
 *    site. Closed-over handlers read the latest prop values directly.
 *
 * 5. `isReactEvent` branches and `.nativeEvent` unwraps drop out: Solid
 *    event handlers receive native events directly.
 *
 * 6. Shadow-DOM-safe `contains()` from `src/internals/dom/contains.ts`.
 *
 * 7. `store.useState('open')` subscribes a Solid tracking context when
 *    read inside `createEffect` / `useIsoLayoutEffect`; the matching
 *    sync escape hatch is `store.state.open` (via
 *    `components/floating-root-store.ts`).
 *
 * 8. The `MaybeAccessor<T>` pattern: callers may pass either a literal
 *    or a `() => T` for the reactive option fields. `readOption`
 *    normalises both at the call site (inside effects/handlers) so the
 *    live value is read at event time.
 *
 * ### Reason strings
 *
 * Every `store.setOpen` emits one of:
 *  - `REASONS.listNavigation` — opening on arrow-down on trigger
 *    (nested / main-orientation paths), or closing via the
 *    cross-orientation "close submenu" key in nested mode.
 *  - `REASONS.focusOut` — Shift+Tab on the floating element.
 *
 * Matches React's source strings 1:1 — the React fork carries these
 * same two in this hook.
 */

import { isHTMLElement } from "@floating-ui/utils/dom";
import { onCleanup, untrack } from "solid-js";
import { createChangeEventDetails, REASONS } from "../../change-event-details.jsx";
import { contains } from "../../dom/contains.jsx";
import { ownerDocument } from "../../dom/owner-document.jsx";
import { createStableCallback } from "../../stable-callback.jsx";
import { useIsoLayoutEffect } from "../../use-iso-layout-effect.jsx";
import { useFloatingParentNodeId, useFloatingTree } from "../components/floating-tree.jsx";
import { createGridCellMap, findNonDisabledListIndex, getGridCellIndexOfCorner, getGridCellIndices, getGridNavigatedIndex, getMaxListIndex, getMinListIndex, isIndexOutOfListBounds, isListIndexDisabled } from "../utils/composite.jsx";
import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from "../utils/constants.jsx";
import { activeElement, getFloatingFocusElement, getTarget, isTypeableCombobox } from "../utils/element.jsx";
import { enqueueFocus } from "../utils/enqueue-focus.jsx";
import { isVirtualClick, isVirtualPointerEvent, stopEvent } from "../utils/event.jsx";
const ESCAPE = "Escape";
import { readOption } from "../../maybe-accessor.jsx";
function doSwitch(orientation, vertical, horizontal) {
  switch (orientation) {
    case "vertical":
      return vertical;
    case "horizontal":
      return horizontal;
    default:
      return vertical || horizontal;
  }
}
function isMainOrientationKey(key, orientation) {
  const vertical = key === ARROW_UP || key === ARROW_DOWN;
  const horizontal = key === ARROW_LEFT || key === ARROW_RIGHT;
  return doSwitch(orientation, vertical, horizontal);
}
function isMainOrientationToEndKey(key, orientation, rtl) {
  const vertical = key === ARROW_DOWN;
  const horizontal = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
  return doSwitch(orientation, vertical, horizontal) || key === "Enter" || key === " " || key === "";
}
function isCrossOrientationOpenKey(key, orientation, rtl) {
  const vertical = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
  const horizontal = key === ARROW_DOWN;
  return doSwitch(orientation, vertical, horizontal);
}
function isCrossOrientationCloseKey(key, orientation, rtl, cols) {
  const vertical = rtl ? key === ARROW_RIGHT : key === ARROW_LEFT;
  const horizontal = key === ARROW_UP;
  if (orientation === "both" || orientation === "horizontal" && cols && cols > 1) {
    return key === ESCAPE;
  }
  return doSwitch(orientation, vertical, horizontal);
}
/**
 * Adds arrow-key-based navigation of a list of items, using real DOM focus
 * or virtual focus via `aria-activedescendant`.
 * @see https://floating-ui.com/docs/useListNavigation
 */
export function useListNavigation(context, props) {
  const store = "rootStore" in context ? context.rootStore : context;
  const dataRef = store.context.dataRef;

  // Untrack the body-time destructure (see use-dismiss for the full
  // reasoning). These fields are not `MaybeAccessor` shapes.
  const {
    listRef,
    onNavigate: onNavigateProp,
    externalTree
  } = untrack(() => ({
    listRef: props.listRef,
    onNavigate: props.onNavigate,
    externalTree: props.externalTree
  }));

  // Option readers — normalise both literal and accessor shapes.
  const getActiveIndex = () => readOption(props.activeIndex);
  const getEnabled = () => props.enabled === undefined ? true : readOption(props.enabled);
  const getSelectedIndex = () => {
    const v = props.selectedIndex;
    if (v === undefined) return null;
    const read = readOption(v);
    return read ?? null;
  };
  const getFocusItemOnOpen = () => props.focusItemOnOpen === undefined ? "auto" : readOption(props.focusItemOnOpen);
  const getFocusItemOnHover = () => props.focusItemOnHover === undefined ? true : readOption(props.focusItemOnHover);
  const getOpenOnArrowKeyDown = () => props.openOnArrowKeyDown === undefined ? true : readOption(props.openOnArrowKeyDown);
  const getDisabledIndices = () => props.disabledIndices === undefined ? undefined : readOption(props.disabledIndices);
  const getAllowEscape = () => props.allowEscape === undefined ? false : readOption(props.allowEscape);
  const getLoopFocus = () => props.loopFocus === undefined ? false : readOption(props.loopFocus);
  const getNested = () => props.nested === undefined ? false : readOption(props.nested);
  const getParentOrientationProp = () => props.parentOrientation === undefined ? undefined : readOption(props.parentOrientation);
  const getRtl = () => props.rtl === undefined ? false : readOption(props.rtl);
  const getVirtual = () => props.virtual === undefined ? false : readOption(props.virtual);
  const getOrientation = () => props.orientation === undefined ? "vertical" : readOption(props.orientation);
  const getCols = () => props.cols === undefined ? 1 : readOption(props.cols);
  const getId = () => props.id === undefined ? undefined : readOption(props.id);
  const getResetOnPointerLeave = () => props.resetOnPointerLeave === undefined ? true : readOption(props.resetOnPointerLeave);
  const parentId = useFloatingParentNodeId();
  const tree = useFloatingTree(externalTree);

  // Mirror React's `dataRef.current.orientation = orientation` layout
  // effect so child list-nav hooks (submenus) can read the parent
  // orientation via the tree.
  useIsoLayoutEffect(() => getOrientation(), orientation => {
    dataRef.current.orientation = orientation;
  });

  // Refs — match React's `useRef` shape for inter-handler communication.
  // Body-time signal reads are wrapped in `untrack` so the strict-read
  // scope established by `devComponent(...)` does not warn. These are
  // one-shot snapshots; reactivity flows through the effects below.
  const focusItemOnOpenRef = {
    current: untrack(() => getFocusItemOnOpen())
  };
  const indexRef = {
    current: untrack(() => getSelectedIndex() ?? -1)
  };
  const keyRef = {
    current: null
  };
  const isPointerModalityRef = {
    current: true
  };
  const previousMountedRef = {
    current: untrack(() => !!store.state.floatingElement)
  };
  const previousOpenRef = {
    current: untrack(() => store.state.open)
  };
  const forceSyncFocusRef = {
    current: false
  };
  const forceScrollIntoViewRef = {
    current: false
  };
  const cancelQueuedFocusRef = {
    current: null
  };
  if (process.env.NODE_ENV !== "production") {
    const allowEscape = getAllowEscape();
    const loopFocus = getLoopFocus();
    const virtual = getVirtual();
    const orientation = getOrientation();
    const cols = getCols();
    if (allowEscape) {
      if (!loopFocus) {
        console.warn("`useListNavigation` looping must be enabled to allow escaping.");
      }
      if (!virtual) {
        console.warn("`useListNavigation` must be virtual to allow escaping.");
      }
    }
    if (orientation === "vertical" && cols > 1) {
      console.warn("In grid list navigation mode (`cols` > 1), the `orientation` should", 'be either "horizontal" or "both".');
    }
  }
  const onNavigate = createStableCallback(() => event => {
    onNavigateProp?.(indexRef.current === -1 ? null : indexRef.current, event);
  });
  const previousOnNavigateRef = {
    current: onNavigate
  };
  const focusItem = createStableCallback(() => () => {
    const virtual = getVirtual();
    const list = listRef.current;
    function runFocus(item) {
      if (virtual) {
        tree?.events.emit("virtualfocus", item);
      } else {
        cancelQueuedFocusRef.current = enqueueFocus(item, {
          sync: forceSyncFocusRef.current,
          preventScroll: true
        });
      }
    }
    const initialItem = list[indexRef.current];
    const forceScrollIntoView = forceScrollIntoViewRef.current;
    if (initialItem) {
      runFocus(initialItem);
    }
    const scheduler = forceSyncFocusRef.current ? v => v() : v => requestAnimationFrame(v);
    scheduler(() => {
      const waitedItem = listRef.current[indexRef.current] || initialItem;
      if (!waitedItem) {
        return;
      }
      if (!initialItem) {
        runFocus(waitedItem);
      }
      const shouldScrollIntoView = waitedItem && (forceScrollIntoView || !isPointerModalityRef.current);
      if (shouldScrollIntoView) {
        // Guard for test environments without `scrollIntoView`.
        waitedItem.scrollIntoView?.({
          block: "nearest",
          inline: "nearest"
        });
      }
    });
  });

  // Sync `selectedIndex` → `activeIndex` on open; reset on close.
  useIsoLayoutEffect(() => {
    if (!getEnabled()) return null;
    return {
      open: store.state.open,
      floatingElement: store.state.floatingElement,
      selectedIndex: getSelectedIndex()
    };
  }, snapshot => {
    if (snapshot == null) return;
    const {
      open,
      floatingElement,
      selectedIndex
    } = snapshot;
    if (open && floatingElement) {
      indexRef.current = selectedIndex ?? -1;
      if (focusItemOnOpenRef.current && selectedIndex != null) {
        forceScrollIntoViewRef.current = true;
        onNavigate();
      }
    } else if (previousMountedRef.current) {
      // `onNavigate` may be swapped conditionally by callers
      // (onNavigate: open ? setActive : setSelected); fire the previous
      // function so the outgoing controller sees the reset.
      indexRef.current = -1;
      previousOnNavigateRef.current();
    }
  });

  // Sync `activeIndex` → focused item while the floating element is open.
  useIsoLayoutEffect(() => {
    if (!getEnabled()) return null;
    // Track active/selected index so an external swap re-runs this
    // effect (matches React's `[activeIndex, selectedIndex, ...]`
    // dep array).
    return {
      open: store.state.open,
      floatingElement: store.state.floatingElement,
      activeIndex: getActiveIndex(),
      selectedIndex: getSelectedIndex(),
      orientation: getOrientation(),
      rtl: getRtl(),
      nested: getNested()
    };
  }, snapshot => {
    if (snapshot == null) return;
    const {
      open,
      floatingElement,
      activeIndex,
      selectedIndex,
      orientation,
      rtl,
      nested
    } = snapshot;
    if (!open) {
      forceSyncFocusRef.current = false;
      return;
    }
    if (!floatingElement) return;
    if (activeIndex == null) {
      forceSyncFocusRef.current = false;
      if (selectedIndex != null) {
        return;
      }

      // Reset while the floating element was open (e.g. list changed).
      if (previousMountedRef.current) {
        indexRef.current = -1;
        focusItem();
      }
      if ((!previousOpenRef.current || !previousMountedRef.current) && focusItemOnOpenRef.current && (keyRef.current != null || focusItemOnOpenRef.current === true && keyRef.current == null)) {
        let runs = 0;
        const waitForListPopulated = () => {
          if (listRef.current[0] == null) {
            if (runs < 2) {
              const scheduler = runs ? v => requestAnimationFrame(v) : v => queueMicrotask(v);
              scheduler(waitForListPopulated);
            }
            runs += 1;
          } else {
            indexRef.current = keyRef.current == null || isMainOrientationToEndKey(keyRef.current, orientation, rtl) || nested ? getMinListIndex(listRef) : getMaxListIndex(listRef);
            keyRef.current = null;
            onNavigate();
          }
        };
        waitForListPopulated();
      }
    } else if (!isIndexOutOfListBounds(listRef.current, activeIndex)) {
      indexRef.current = activeIndex;
      focusItem();
      forceScrollIntoViewRef.current = false;
    }
  });

  // Ensure parent floating has focus when a nested child closes, so arrow
  // navigation keeps working once the pointer leaves the child.
  useIsoLayoutEffect(() => ({
    floatingElement: store.state.floatingElement,
    virtual: getVirtual(),
    enabled: getEnabled()
  }), snapshot => {
    const {
      floatingElement,
      virtual,
      enabled
    } = snapshot;
    if (!enabled || floatingElement || !tree || virtual || !previousMountedRef.current) {
      return;
    }

    // Point-in-time reads of other tree nodes' (reactive) elements.
    const nodes = tree.nodesRef.current;
    const parent = untrack(() => nodes.find(node => node.id === parentId)?.context?.elements.floating);
    const activeEl = activeElement(ownerDocument(floatingElement));
    const treeContainsActiveEl = untrack(() => nodes.some(node => node.context && contains(node.context.elements.floating, activeEl)));
    if (parent && !treeContainsActiveEl && isPointerModalityRef.current) {
      parent.focus({
        preventScroll: true
      });
    }
  });

  // Mirror React's trailing `useIsoLayoutEffect` that captures the most
  // recent onNavigate/open/mount values for later reset comparisons.
  useIsoLayoutEffect(() => ({
    open: store.state.open,
    floatingElement: store.state.floatingElement
  }), snapshot => {
    previousOnNavigateRef.current = onNavigate;
    previousOpenRef.current = snapshot.open;
    previousMountedRef.current = !!snapshot.floatingElement;
  });

  // Reset `keyRef` and refresh the `focusItemOnOpen` snapshot on close.
  useIsoLayoutEffect(() => ({
    open: store.state.open,
    focusItemOnOpen: getFocusItemOnOpen()
  }), snapshot => {
    if (!snapshot.open) {
      keyRef.current = null;
      focusItemOnOpenRef.current = snapshot.focusItemOnOpen;
    }
  });

  // Component-unmount cleanup — mirrors nothing specific in React, but
  // matches other hooks' cancellation pattern. Cancels any pending focus
  // request so a detached list doesn't yank focus.
  onCleanup(() => {
    cancelQueuedFocusRef.current?.();
    cancelQueuedFocusRef.current = null;
  });
  const syncCurrentTarget = createStableCallback(() => event => {
    if (!store.state.open) return;
    const list = listRef.current;
    const index = list.indexOf(event.currentTarget);
    // Snapshot for an imperative comparison — this focus handler can run
    // synchronously from an effect-apply scope (via `enqueueFocus`), where a
    // tracked `props.activeIndex` read would trip `[STRICT_READ_UNTRACKED]`.
    const activeIndex = untrack(getActiveIndex);
    if (index !== -1 && (indexRef.current !== index || activeIndex !== index)) {
      indexRef.current = index;
      onNavigate(event);
    }
  });
  const item = {
    onFocus(event) {
      forceSyncFocusRef.current = true;
      syncCurrentTarget(event);
    },
    // Safari focus workaround — re-focus on click.
    onClick(event) {
      event.currentTarget.focus({
        preventScroll: true
      });
    },
    onMouseMove(event) {
      forceSyncFocusRef.current = true;
      forceScrollIntoViewRef.current = false;
      if (getFocusItemOnHover()) {
        syncCurrentTarget(event);
      }
    },
    onPointerLeave(event) {
      if (!store.state.open || !isPointerModalityRef.current || event.pointerType === "touch") {
        return;
      }
      forceSyncFocusRef.current = true;
      const relatedTarget = event.relatedTarget;
      if (!getFocusItemOnHover() || listRef.current.includes(relatedTarget)) {
        return;
      }
      if (!getResetOnPointerLeave()) {
        return;
      }
      cancelQueuedFocusRef.current?.();
      cancelQueuedFocusRef.current = null;
      indexRef.current = -1;
      onNavigate(event);
      if (!getVirtual()) {
        const floatingFocusEl = getFloatingFocusElement(store.state.floatingElement);
        const activeEl = activeElement(ownerDocument(floatingFocusEl));
        if (floatingFocusEl && contains(floatingFocusEl, activeEl)) {
          floatingFocusEl.focus({
            preventScroll: true
          });
        }
      }
    }
  };
  const getParentOrientation = () => {
    const fromProp = getParentOrientationProp();
    if (fromProp !== undefined) return fromProp;
    return tree?.nodesRef.current.find(node => node.id === parentId)?.context?.dataRef?.current.orientation ?? undefined;
  };
  const commonOnKeyDown = createStableCallback(() => event => {
    isPointerModalityRef.current = false;
    forceSyncFocusRef.current = true;

    // Chrome fires ArrowDown twice while composing. Ignore the IME path.
    if (event.which === 229) {
      return;
    }
    const floatingFocusEl = getFloatingFocusElement(store.state.floatingElement);

    // Floating element animating out: ignore so `activeIndex` doesn't
    // latch while closed.
    if (!store.state.open && event.currentTarget === floatingFocusEl) {
      return;
    }

    // beta.28 can deliver the first key event before the layout effect
    // that mirrors an externally-seeded active index into this imperative
    // cursor. Seed it on demand so initial auto-highlight advances from
    // item 0 instead of selecting item 0 again.
    if (indexRef.current === -1) {
      const activeIndex = getActiveIndex();
      if (activeIndex != null && !isIndexOutOfListBounds(listRef.current, activeIndex)) {
        indexRef.current = activeIndex;
      }
    }
    const orientation = getOrientation();
    const rtl = getRtl();
    const nested = getNested();
    const cols = getCols();
    const virtual = getVirtual();
    const disabledIndices = getDisabledIndices();
    const loopFocus = getLoopFocus();
    const allowEscape = getAllowEscape();
    if (nested && isCrossOrientationCloseKey(event.key, orientation, rtl, cols)) {
      // If the nested close key matches the parent navigation key, let
      // the parent navigate; otherwise swallow the event.
      if (!isMainOrientationKey(event.key, getParentOrientation() ?? "vertical")) {
        stopEvent(event);
      }
      store.setOpen(false, createChangeEventDetails(REASONS.listNavigation, event));
      const domReferenceElement = store.state.domReferenceElement;
      if (isHTMLElement(domReferenceElement)) {
        if (virtual) {
          tree?.events.emit("virtualfocus", domReferenceElement);
        } else {
          domReferenceElement.focus();
        }
      }
      return;
    }
    const currentIndex = indexRef.current;
    const minIndex = getMinListIndex(listRef, disabledIndices);
    const maxIndex = getMaxListIndex(listRef, disabledIndices);
    const domReferenceElement = store.state.domReferenceElement;
    const typeableComboboxReference = isTypeableCombobox(domReferenceElement);
    if (!typeableComboboxReference) {
      if (event.key === "Home") {
        stopEvent(event);
        indexRef.current = minIndex;
        onNavigate(event);
      }
      if (event.key === "End") {
        stopEvent(event);
        indexRef.current = maxIndex;
        onNavigate(event);
      }
    }

    // Grid navigation.
    if (cols > 1) {
      const sizes = Array.from({
        length: listRef.current.length
      }, () => ({
        width: 1,
        height: 1
      }));
      const cellMap = createGridCellMap(sizes, cols, false);
      const minGridIndex = cellMap.findIndex(index => index != null && !isListIndexDisabled(listRef.current, index, disabledIndices));
      const maxGridIndex = cellMap.reduce((foundIndex, index, cellIndex) => index != null && !isListIndexDisabled(listRef.current, index, disabledIndices) ? cellIndex : foundIndex, -1);
      const index = cellMap[getGridNavigatedIndex(cellMap.map(itemIndex => itemIndex != null ? listRef.current[itemIndex] : null), {
        event,
        orientation,
        loopFocus,
        rtl,
        cols,
        disabledIndices: getGridCellIndices([...((typeof disabledIndices !== "function" ? disabledIndices : null) || listRef.current.map((_, listIndex) => isListIndexDisabled(listRef.current, listIndex, disabledIndices) ? listIndex : undefined)), undefined], cellMap),
        minIndex: minGridIndex,
        maxIndex: maxGridIndex,
        prevIndex: getGridCellIndexOfCorner(indexRef.current > maxIndex ? minIndex : indexRef.current, sizes, cellMap, cols, event.key === ARROW_DOWN ? "bl" : event.key === (rtl ? ARROW_LEFT : ARROW_RIGHT) ? "tr" : "tl"),
        stopEvent: true
      })];
      if (index != null) {
        indexRef.current = index;
        onNavigate(event);
      }
      if (orientation === "both") {
        return;
      }
    }
    if (isMainOrientationKey(event.key, orientation)) {
      stopEvent(event);
      const ct = event.currentTarget;
      // Reset if no item is focused.
      if (store.state.open && !virtual && ct && activeElement(ct.ownerDocument) === ct) {
        indexRef.current = isMainOrientationToEndKey(event.key, orientation, rtl) ? minIndex : maxIndex;
        onNavigate(event);
        return;
      }
      if (isMainOrientationToEndKey(event.key, orientation, rtl)) {
        if (loopFocus) {
          if (currentIndex >= maxIndex) {
            if (allowEscape && currentIndex !== listRef.current.length) {
              indexRef.current = -1;
            } else {
              // Give time for virtualizers to update the listRef.
              forceSyncFocusRef.current = false;
              indexRef.current = minIndex;
            }
          } else {
            indexRef.current = findNonDisabledListIndex(listRef.current, {
              startingIndex: currentIndex,
              disabledIndices
            });
          }
        } else {
          indexRef.current = Math.min(maxIndex, findNonDisabledListIndex(listRef.current, {
            startingIndex: currentIndex,
            disabledIndices
          }));
        }
      } else if (loopFocus) {
        if (currentIndex <= minIndex) {
          if (allowEscape && currentIndex !== -1) {
            indexRef.current = listRef.current.length;
          } else {
            forceSyncFocusRef.current = false;
            indexRef.current = maxIndex;
          }
        } else {
          indexRef.current = findNonDisabledListIndex(listRef.current, {
            startingIndex: currentIndex,
            decrement: true,
            disabledIndices
          });
        }
      } else {
        indexRef.current = Math.max(minIndex, findNonDisabledListIndex(listRef.current, {
          startingIndex: currentIndex,
          decrement: true,
          disabledIndices
        }));
      }
      if (isIndexOutOfListBounds(listRef.current, indexRef.current)) {
        indexRef.current = -1;
      }
      onNavigate(event);
    }
  });
  const floating = {
    get "aria-orientation"() {
      const orientation = getOrientation();
      return orientation === "both" ? undefined : orientation;
    },
    // `aria-activedescendant` is wired reactively so consumers see the
    // current index without re-running the component body. The `virtual`
    // gate is checked inside.
    get "aria-activedescendant"() {
      if (!getVirtual() || !store.state.open) return undefined;
      const activeIndex = getActiveIndex();
      if (activeIndex == null) return undefined;
      const typeableComboboxReference = isTypeableCombobox(store.state.domReferenceElement);
      if (typeableComboboxReference) return undefined;
      const id = getId();
      return `${id}-${activeIndex}`;
    },
    onKeyDown(event) {
      const virtual = getVirtual();
      const open = store.state.open;

      // Close submenu on Shift+Tab
      if (event.key === "Tab" && event.shiftKey && open && !virtual) {
        const target = getTarget(event);
        const floatingFocusEl = getFloatingFocusElement(store.state.floatingElement);
        // If originated within a nested element, let the nested focus
        // manager handle Tab.
        if (target && !contains(floatingFocusEl, target)) {
          return;
        }
        stopEvent(event);
        store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event));
        const domReferenceElement = store.state.domReferenceElement;
        if (isHTMLElement(domReferenceElement)) {
          domReferenceElement.focus();
        }
        return;
      }
      commonOnKeyDown(event);
    },
    onPointerMove() {
      isPointerModalityRef.current = true;
    }
  };
  const triggerKeyDown = event => {
    // Non-reactive open snapshot (matches React's `store.select('open')`).
    const currentOpen = store.state.open;
    isPointerModalityRef.current = false;
    const orientation = getOrientation();
    const rtl = getRtl();
    const nested = getNested();
    const virtual = getVirtual();
    const openOnArrowKeyDown = getOpenOnArrowKeyDown();
    const isArrowKey = event.key.startsWith("Arrow");
    const isParentCrossOpenKey = isCrossOrientationOpenKey(event.key, getParentOrientation() ?? "vertical", rtl);
    const isMainKey = isMainOrientationKey(event.key, orientation);
    const isNavigationKey = (nested ? isParentCrossOpenKey : isMainKey) || event.key === "Enter" || event.key.trim() === "";
    if (virtual && currentOpen) {
      return commonOnKeyDown(event);
    }

    // Don't stash `activeIndex` while closed if arrow-key-down is opt-out.
    if (!currentOpen && !openOnArrowKeyDown && isArrowKey) {
      return undefined;
    }
    if (isNavigationKey) {
      const isParentMainKey = isMainOrientationKey(event.key, getParentOrientation() ?? "vertical");
      keyRef.current = nested && isParentMainKey ? null : event.key;
    }
    if (nested) {
      if (isParentCrossOpenKey) {
        stopEvent(event);
        if (currentOpen) {
          indexRef.current = getMinListIndex(listRef, getDisabledIndices());
          onNavigate(event);
        } else {
          store.setOpen(true, createChangeEventDetails(REASONS.listNavigation, event, event.currentTarget));
        }
      }
      return undefined;
    }
    if (isMainKey) {
      const selectedIndex = getSelectedIndex();
      if (selectedIndex != null) {
        indexRef.current = selectedIndex;
      }
      stopEvent(event);
      if (!currentOpen && openOnArrowKeyDown) {
        store.setOpen(true, createChangeEventDetails(REASONS.listNavigation, event, event.currentTarget));
      } else {
        commonOnKeyDown(event);
      }
      if (currentOpen) {
        onNavigate(event);
      }
    }
    return undefined;
  };
  const checkVirtualMouse = event => {
    if (getFocusItemOnOpen() === "auto" && isVirtualClick(event)) {
      focusItemOnOpenRef.current = !getVirtual();
    }
  };
  const checkVirtualPointer = event => {
    // `pointerdown` fires first; reset then check.
    focusItemOnOpenRef.current = getFocusItemOnOpen();
    if (getFocusItemOnOpen() === "auto" && isVirtualPointerEvent(event)) {
      focusItemOnOpenRef.current = true;
    }
  };
  const trigger = {
    onKeyDown: triggerKeyDown,
    onFocus(event) {
      if (store.state.open && !getVirtual()) {
        indexRef.current = -1;
        onNavigate(event);
      }
    },
    onPointerDown: checkVirtualPointer,
    onPointerEnter: checkVirtualPointer,
    onMouseDown: checkVirtualMouse,
    onClick: checkVirtualMouse
  };
  const reference = {
    // `aria-activedescendant` on the reference for virtual/combobox cases.
    get "aria-activedescendant"() {
      if (!getVirtual() || !store.state.open) return undefined;
      const activeIndex = getActiveIndex();
      if (activeIndex == null) return undefined;
      const id = getId();
      return `${id}-${activeIndex}`;
    },
    onKeyDown: triggerKeyDown,
    onFocus(event) {
      if (store.state.open && !getVirtual()) {
        indexRef.current = -1;
        onNavigate(event);
      }
    },
    onPointerDown: checkVirtualPointer,
    onPointerEnter: checkVirtualPointer,
    onMouseDown: checkVirtualMouse,
    onClick: checkVirtualMouse
  };

  // Signalled enablement: when `enabled` is a literal `false`, React
  // returns `{}`. When it's an accessor, we always return the bag so
  // the attached handlers short-circuit internally when disabled. This
  // matches the hover/dismiss approach and preserves the "flip enabled
  // off" contract under tier-3a. Untrack the body-time `props.enabled`
  // access so the strict-read scope does not warn — accessor reads
  // flow through tracked effects/handlers below.
  const isStaticallyDisabled = untrack(() => typeof props.enabled !== "function" && !getEnabled());
  if (isStaticallyDisabled) {
    return {};
  }
  return {
    reference,
    floating,
    item,
    trigger
  };
}
//# sourceMappingURL=use-list-navigation.jsx.map
