/**
 * Stage 5.2 port of Base UI React's `FloatingFocusManager`
 * (`base-ui/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx`).
 *
 * Despite living at `components/FocusTrap.tsx`-style paths in older
 * drafts, the React source filename used here is `FloatingFocusManager.tsx`
 * — confirmed by the `export function FloatingFocusManager(...)` declaration.
 *
 * Feature parity (1:1 with React):
 *  - Invisible `FocusGuard` sentinels (before / after the floating subtree)
 *    that catch Tab / Shift+Tab at the trap boundaries and redirect focus
 *    (wrap-around inside a modal; jump to the next / previous tabbable outside
 *    the portal for non-modal).
 *  - `initialFocus`: `boolean | HTMLElement | RefObject<HTMLElement> |
 *    ((type) => ...)`. `false` skips auto-focus; default picks the first
 *    tabbable element inside the floating container (or the container
 *    itself when empty).
 *  - `returnFocus`: same shape — restores focus to the trigger (or the
 *    `getPreviouslyFocusedElement()` fallback) when the floating element
 *    unmounts / closes. Virtual clicks (and `mouseleave`-driven hover close)
 *    skip return-focus to avoid unwanted viewport scrolls.
 *  - `restoreFocus`: `boolean | 'popup'`. When focus gets lost inside the
 *    subtree (e.g. the previously focused element is removed mid-session),
 *    restores to the last tabbable — or directly to the floating container
 *    with `'popup'`.
 *  - `modal`: modal (true) traps focus using `markOthers` — siblings get
 *    `aria-hidden="true"` plus the `data-quoin-inert` marker. Non-modal
 *    (false) only manages initial / return focus + optional
 *    `closeOnFocusOut`.
 *  - `closeOnFocusOut`: when focus escapes the floating subtree (and the
 *    target isn't a sibling trigger, portal guard, child floating node, or
 *    ancestor combobox), dispatches `setOpen(false, ..., 'focus-out')`.
 *  - `disabled`: full no-op pass-through.
 *  - `nextFocusableElement` / `previousFocusableElement`: overrides for the
 *    outside Tab / Shift+Tab targets (used by combobox / navigation-menu).
 *  - `beforeContentFocusGuardRef`: external ref to the before-guard so
 *    consumers can focus the popup programmatically.
 *  - `externalTree`: escape-hatch `FloatingTreeStore` override (matches
 *    `useDismiss`).
 *  - `getInsideElements`: additional elements to treat as part of the
 *    floating subtree for `aria-hidden` / focus-escape calculations.
 *  - Nested-tree pause: the effect that installs document listeners is
 *    keyed on `open` and on the floating-focus element; child
 *    `FloatingFocusManager`s nest inside the same tree and each install
 *    their own listeners — the parent's `closeOnFocusOut` check excludes
 *    descendants via `getNodeChildren()`, so a focus move into a nested
 *    open floating element does NOT trigger the parent's close-on-focus-out.
 *    This mirrors React 1:1.
 *
 * ### React → Solid deviations
 *
 * 1. `useRef` → `MutableRef<T>` (plain `{ current }` shape).
 * 2. `useEffect` → `createEffect` + `onCleanup`. Each original React
 *    effect is mapped to a Solid `createEffect` whose tracked reads
 *    replace the React dep array.
 * 3. `useLayoutEffect` → `useIsoLayoutEffect` (alias of
 *    `createRenderEffect`), which runs compute / apply synchronously
 *    during the render phase — matching `useLayoutEffect` timing.
 * 4. `useMergedRefs` has no Solid equivalent — ref callbacks are just
 *    plain functions here. Each guard sets its own internal slot plus
 *    propagates to the consumer's `beforeContentFocusGuardRef` or the
 *    `usePortalContext()` before/after-inside refs.
 * 5. `useValueAsRef` collapses to plain variables captured by closure
 *    — Solid component bodies don't re-render, so the props reference
 *    captured at setup is the always-current one. For props that might
 *    be reactive (Solid signals on parent components), we read them
 *    through a stable callback.
 * 6. `useStableCallback` → `createStableCallback`.
 * 7. Shadow-DOM-safe `contains()` / `getTarget` / `activeElement` from
 *    `src/internals/dom/*`.
 * 8. React's `FocusGuard` component is re-implemented inline as the
 *    `FocusGuard` helper (no separate file — it's tiny).
 * 9. The React port imports `usePortalContext` from `./FloatingPortal`.
 *    The Solid port's `FloatingPortal` is being ported in parallel
 *    (Stage 5.1); to keep this file unblocked, we declare a local
 *    `FloatingPortalContext` (a null-default `createContext`) +
 *    `usePortalContext` helper here. When the sibling port lands it
 *    will re-export these (or adopt the same shape) so the import path
 *    stays stable. Follow-up: point `FloatingPortal.tsx` at this
 *    context or vice-versa once both are merged.
 * 10. The `openInteractionType` prop is accepted; our `InteractionType`
 *     type mirrors Base UI React's `useEnhancedClickHandler`-declared
 *     union. Because `@base-ui/utils` isn't available in the Solid port,
 *     we inline the union here.
 *
 * Reason strings dispatched:
 *   - `store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event))`
 *     on focus-out (matches `"focus-out"` — see `REASONS.focusOut` in
 *     `src/internals/change-event-details.ts`).
 *
 * Deferrals: real-browser :focus-visible, real focus-scroll prevention
 * probing inside iframes, portal-sentinel integration validation.
 */

import { getNodeName, isHTMLElement, isWebKit } from "@floating-ui/utils/dom";
import { createEffect, onCleanup } from "solid-js";
import { createChangeEventDetails, REASONS } from "../../change-event-details.jsx";
import { activeElement } from "../../dom/active-element.jsx";
import { contains } from "../../dom/contains.jsx";
import { getTarget } from "../../dom/get-target.jsx";
import { ownerDocument } from "../../dom/owner-document.jsx";
import { ownerWindow } from "../../dom/owner-window.jsx";
import { FocusGuard } from "../../popups/focus-guard.jsx";
import { createStableCallback } from "../../stable-callback.jsx";
import { AnimationFrame } from "../../use-animation-frame.jsx";
import { useIsoLayoutEffect } from "../../use-iso-layout-effect.jsx";
import { Timeout } from "../../use-timeout.jsx";
import { createAttribute } from "../utils/create-attribute.jsx";
import { getFloatingFocusElement, isTypeableCombobox, isTypeableElement } from "../utils/element.jsx";
import { enqueueFocus } from "../utils/enqueue-focus.jsx";
import { isVirtualClick, isVirtualPointerEvent, stopEvent } from "../utils/event.jsx";
import { markOthers } from "../utils/mark-others.jsx";
import { getNodeAncestors, getNodeChildren } from "../utils/nodes.jsx";
import { focusable, getNextTabbable, getPreviousTabbable, isOutsideEvent, isTabbable, tabbable } from "../utils/tabbable.jsx";
import { usePortalContext } from "./floating-portal.jsx";
import { useFloatingTree } from "./floating-tree.jsx";

// ---------------------------------------------------------------------------
// Interaction type — inlined from Base UI React's `useEnhancedClickHandler`.
// ---------------------------------------------------------------------------

/**
 * Interaction type used to open / close a floating element. Mirrors
 * `InteractionType` from `@base-ui/utils/useEnhancedClickHandler`.
 */

// ---------------------------------------------------------------------------
// Local helpers — 1:1 with the React source.
// ---------------------------------------------------------------------------

function resolveRef(maybeRef) {
  if (maybeRef == null) {
    return null;
  }
  if (typeof maybeRef === "object" && "current" in maybeRef) {
    return maybeRef.current;
  }
  return maybeRef;
}
function getEventType(event, lastInteractionType) {
  const win = ownerWindow(getTarget(event));
  if (event instanceof win.KeyboardEvent) {
    return "keyboard";
  }
  if (event instanceof win.FocusEvent) {
    return lastInteractionType || "keyboard";
  }
  if ("pointerType" in event) {
    return event.pointerType || "keyboard";
  }
  if ("touches" in event) {
    return "touch";
  }
  if (event instanceof win.MouseEvent) {
    return lastInteractionType || (event.detail === 0 ? "keyboard" : "mouse");
  }
  return "";
}
const LIST_LIMIT = 20;
let previouslyFocusedElements = [];
function clearDisconnectedPreviouslyFocusedElements() {
  previouslyFocusedElements = previouslyFocusedElements.filter(entry => entry.deref()?.isConnected);
}
function addPreviouslyFocusedElement(element) {
  clearDisconnectedPreviouslyFocusedElements();
  if (element && getNodeName(element) !== "body") {
    previouslyFocusedElements.push(new WeakRef(element));
    if (previouslyFocusedElements.length > LIST_LIMIT) {
      previouslyFocusedElements = previouslyFocusedElements.slice(-LIST_LIMIT);
    }
  }
}
function getPreviouslyFocusedElement() {
  clearDisconnectedPreviouslyFocusedElements();
  return previouslyFocusedElements[previouslyFocusedElements.length - 1]?.deref();
}
function getFirstTabbableElement(container) {
  if (!container) {
    return null;
  }
  if (isTabbable(container)) {
    return container;
  }
  return tabbable(container)[0] || container;
}
function isFocusable(element) {
  if (!element?.isConnected) {
    return false;
  }
  if (typeof element.checkVisibility === "function") {
    return element.checkVisibility();
  }
  // Fall back to the bounding-client-rect heuristic. `isElementVisible` lives
  // in `utils/composite.ts` but isn't re-exported by the element bundle —
  // accessing the raw rect here keeps the dependency graph small.
  const rect = element.getBoundingClientRect?.();
  return !!rect && rect.width > 0 && rect.height > 0;
}
function handleTabIndex(floatingFocusElement, orderRef) {
  if (floatingFocusElement.hasAttribute("tabindex") && !floatingFocusElement.hasAttribute("data-tabindex")) {
    return;
  }
  if (!orderRef.current.includes("floating") && !floatingFocusElement.getAttribute("role")?.includes("dialog")) {
    return;
  }
  const focusableElements = focusable(floatingFocusElement);
  const tabbableContent = focusableElements.filter(element => {
    const dataTabIndex = element.getAttribute("data-tabindex") || "";
    return isTabbable(element) || element.hasAttribute("data-tabindex") && !dataTabIndex.startsWith("-");
  });
  const tabIndex = floatingFocusElement.getAttribute("tabindex");
  if (orderRef.current.includes("floating") || tabbableContent.length === 0) {
    if (tabIndex !== "0") {
      floatingFocusElement.setAttribute("tabindex", "0");
    }
  } else if (tabIndex !== "-1" || floatingFocusElement.hasAttribute("data-tabindex") && floatingFocusElement.getAttribute("data-tabindex") !== "-1") {
    floatingFocusElement.setAttribute("tabindex", "-1");
    floatingFocusElement.setAttribute("data-tabindex", "-1");
  }
}

// ---------------------------------------------------------------------------
// `FloatingFocusManager`
//
// Trap-boundary `FocusGuard` sentinels come from the shared module at
// `src/internals/popups/focus-guard.tsx` (imported above), which handles
// the Safari `role="button"` VoiceOver workaround that the earlier inline
// helper deferred.
// ---------------------------------------------------------------------------

const CLICK_TRIGGER_IDENTIFIER = "data-quoin-click-trigger";

/**
 * Provides focus management for the floating element.
 * @see https://floating-ui.com/docs/FloatingFocusManager
 * @internal
 */
export function FloatingFocusManager(props) {
  // Resolve-once destructuring with defaults (Solid component bodies
  // run once; reactive prop reads happen through the store).
  const disabled = () => props.disabled ?? false;
  const initialFocus = () => props.initialFocus ?? true;
  const returnFocusProp = () => props.returnFocus ?? true;
  const restoreFocus = () => props.restoreFocus ?? false;
  const modal = () => props.modal ?? true;
  const closeOnFocusOut = () => props.closeOnFocusOut ?? true;
  const openInteractionType = () => props.openInteractionType ?? "";
  const rootStore = () => "rootStore" in props.context ? props.context.rootStore : props.context;
  const dataRef = rootStore().context.dataRef;
  const events = rootStore().context.events;
  const getNodeId = createStableCallback(() => () => dataRef.current.floatingContext?.nodeId);
  const ignoreInitialFocus = () => initialFocus() === false;
  const isUntrappedTypeableCombobox = () => isTypeableCombobox(rootStore().useState("domReferenceElement")) && ignoreInitialFocus();
  const orderRef = {
    current: ["content"]
  };
  const tree = useFloatingTree(props.externalTree);
  const portalContext = usePortalContext();
  const preventReturnFocusRef = {
    current: false
  };
  const isPointerDownRef = {
    current: false
  };
  const pointerDownOutsideRef = {
    current: false
  };
  const lastFocusedTabbableRef = {
    current: null
  };
  const closeTypeRef = {
    current: ""
  };
  const lastInteractionTypeRef = {
    current: ""
  };
  const beforeGuardRef = {
    current: null
  };
  const afterGuardRef = {
    current: null
  };
  const blurTimeout = new Timeout();
  const pointerDownTimeout = new Timeout();
  const restoreFocusFrame = new AnimationFrame();
  onCleanup(() => {
    blurTimeout.clear();
    pointerDownTimeout.clear();
    restoreFocusFrame.cancel();
  });
  const isInsidePortal = () => portalContext != null;
  const getFloatingFocus = () => getFloatingFocusElement(rootStore().useState("floatingElement"));
  const getTabbableContent = createStableCallback(() => (container = getFloatingFocus()) => container ? tabbable(container) : []);
  const getResolvedInsideElements = createStableCallback(() => () => props.getInsideElements?.().filter(element => element != null) ?? []);

  // Prevent Tab from escaping the modal when there are no tabbable elements.
  // NOTE: in Solid 2, `onCleanup` registered inside the EFFECT function of the
  // two-arg `createEffect(compute, effect)` form does not fire on dispose.
  // All setup + onCleanup therefore lives in the COMPUTE function; the effect
  // argument is a no-op.
  createEffect(() => {
    const floatingFocusElement = getFloatingFocus();
    const isDisabled = disabled();
    const isModal = modal();
    if (isDisabled || !isModal) {
      return undefined;
    }
    function onKeyDown(event) {
      if (event.key === "Tab") {
        if (contains(floatingFocusElement, activeElement(ownerDocument(floatingFocusElement))) && getTabbableContent().length === 0 && !isUntrappedTypeableCombobox()) {
          stopEvent(event);
        }
      }
    }
    const doc = ownerDocument(floatingFocusElement);
    doc.addEventListener("keydown", onKeyDown);
    onCleanup(() => {
      doc.removeEventListener("keydown", onKeyDown);
    });
    return undefined;
  }, () => {});

  // Track pointer / keyboard interactions to disambiguate focus origins.
  createEffect(() => {
    const open = rootStore().useState("open");
    const floating = rootStore().useState("floatingElement");
    const floatingFocusElement = getFloatingFocus();
    const domReference = rootStore().useState("domReferenceElement");
    const isDisabled = disabled();
    if (isDisabled || !open) {
      return undefined;
    }
    const doc = ownerDocument(floatingFocusElement);
    function clearPointerDownOutside() {
      pointerDownOutsideRef.current = false;
    }
    function onPointerDown(event) {
      const target = getTarget(event);
      const insideElements = getResolvedInsideElements();
      const pointerTargetInside = contains(floating, target) || contains(domReference, target) || contains(portalContext?.portalNode ?? null, target) || insideElements.some(element => element === target || contains(element, target));
      pointerDownOutsideRef.current = !pointerTargetInside;
      lastInteractionTypeRef.current = event.pointerType || "keyboard";
      if (target?.closest?.(`[${CLICK_TRIGGER_IDENTIFIER}]`)) {
        isPointerDownRef.current = true;
      }
    }
    function onKeyDown() {
      lastInteractionTypeRef.current = "keyboard";
    }
    doc.addEventListener("pointerdown", onPointerDown, true);
    doc.addEventListener("pointerup", clearPointerDownOutside, true);
    doc.addEventListener("pointercancel", clearPointerDownOutside, true);
    doc.addEventListener("keydown", onKeyDown, true);
    onCleanup(() => {
      doc.removeEventListener("pointerdown", onPointerDown, true);
      doc.removeEventListener("pointerup", clearPointerDownOutside, true);
      doc.removeEventListener("pointercancel", clearPointerDownOutside, true);
      doc.removeEventListener("keydown", onKeyDown, true);
    });
    return undefined;
  }, () => {});

  // Close on focus out and restore focus within the floating tree when
  // needed.
  createEffect(() => {
    const floating = rootStore().useState("floatingElement");
    const domReference = rootStore().useState("domReferenceElement");
    const floatingFocusElement = getFloatingFocus();
    const isDisabled = disabled();
    const isCloseOnFocusOut = closeOnFocusOut();
    if (isDisabled || !isCloseOnFocusOut) {
      return undefined;
    }
    const doc = ownerDocument(floatingFocusElement);
    function handlePointerDown() {
      isPointerDownRef.current = true;
      pointerDownTimeout.start(0, () => {
        isPointerDownRef.current = false;
      });
    }
    function handleFocusIn(event) {
      const target = getTarget(event);
      if (isTabbable(target)) {
        lastFocusedTabbableRef.current = target;
      }
    }
    function handleFocusOutside(event) {
      const relatedTarget = event.relatedTarget;
      const currentTarget = event.currentTarget;
      const target = getTarget(event);
      queueMicrotask(() => {
        const nodeId = getNodeId();
        const triggers = rootStore().context.triggerElements;
        const insideElements = getResolvedInsideElements();
        const isRelatedFocusGuard = !!relatedTarget?.hasAttribute(createAttribute("focus-guard")) && [beforeGuardRef.current, afterGuardRef.current, portalContext?.beforeInsideRef.current ?? null, portalContext?.afterInsideRef.current ?? null, portalContext?.beforeOutsideRef.current ?? null, portalContext?.afterOutsideRef.current ?? null, resolveRef(props.previousFocusableElement), resolveRef(props.nextFocusableElement)].includes(relatedTarget);
        const movedToUnrelatedNode = !(contains(domReference, relatedTarget) || contains(floating, relatedTarget) || contains(relatedTarget, floating) || contains(portalContext?.portalNode ?? null, relatedTarget) || insideElements.some(element => element === relatedTarget || contains(element, relatedTarget)) || relatedTarget != null && triggers.hasElement(relatedTarget) || triggers.hasMatchingElement(trigger => contains(trigger, relatedTarget)) || isRelatedFocusGuard || tree && (getNodeChildren(tree.nodesRef.current, nodeId).find(node => contains(node.context?.elements.floating, relatedTarget) || contains(node.context?.elements.domReference, relatedTarget)) || getNodeAncestors(tree.nodesRef.current, nodeId).find(node => [node.context?.elements.floating, getFloatingFocusElement(node.context?.elements.floating)].includes(relatedTarget) || node.context?.elements.domReference === relatedTarget)));
        if (currentTarget === domReference && floatingFocusElement) {
          handleTabIndex(floatingFocusElement, orderRef);
        }

        // Restore focus to a safe slot inside the floating tree when
        // focus is lost (e.g. active element removed from the DOM).
        if (restoreFocus() && currentTarget !== domReference && !isFocusable(target) && activeElement(doc) === doc.body) {
          if (isHTMLElement(floatingFocusElement)) {
            floatingFocusElement.focus();
            if (restoreFocus() === "popup") {
              restoreFocusFrame.request(() => {
                floatingFocusElement.focus();
              });
              return;
            }
          }
          const tabbableContent = getTabbableContent();
          const prevTabbable = lastFocusedTabbableRef.current;
          const nodeToFocus = (prevTabbable && tabbableContent.includes(prevTabbable) ? prevTabbable : null) || tabbableContent[tabbableContent.length - 1] || floatingFocusElement;
          if (isHTMLElement(nodeToFocus)) {
            nodeToFocus.focus();
          }
        }
        if (dataRef.current.insideReactTree) {
          dataRef.current.insideReactTree = false;
          return;
        }
        if ((isUntrappedTypeableCombobox() ? true : !modal()) && relatedTarget && movedToUnrelatedNode && !isPointerDownRef.current && (isUntrappedTypeableCombobox() || relatedTarget !== getPreviouslyFocusedElement())) {
          preventReturnFocusRef.current = true;
          rootStore().setOpen(false, createChangeEventDetails(REASONS.focusOut, event));
        }
      });
    }
    function markInsideReactTree() {
      if (pointerDownOutsideRef.current) {
        return;
      }
      dataRef.current.insideReactTree = true;
      blurTimeout.start(0, () => {
        dataRef.current.insideReactTree = false;
      });
    }
    const domReferenceElement = isHTMLElement(domReference) ? domReference : null;
    if (!floating && !domReferenceElement) {
      return;
    }
    const cleanups = [];
    if (domReferenceElement) {
      domReferenceElement.addEventListener("focusout", handleFocusOutside);
      domReferenceElement.addEventListener("pointerdown", handlePointerDown);
      cleanups.push(() => {
        domReferenceElement.removeEventListener("focusout", handleFocusOutside);
        domReferenceElement.removeEventListener("pointerdown", handlePointerDown);
      });
    }
    if (floating) {
      floating.addEventListener("focusin", handleFocusIn);
      floating.addEventListener("focusout", handleFocusOutside);
      cleanups.push(() => {
        floating.removeEventListener("focusin", handleFocusIn);
        floating.removeEventListener("focusout", handleFocusOutside);
      });
      if (portalContext) {
        floating.addEventListener("focusout", markInsideReactTree, true);
        cleanups.push(() => {
          floating.removeEventListener("focusout", markInsideReactTree, true);
        });
      }
    }
    onCleanup(() => {
      for (const cleanup of cleanups) {
        cleanup();
      }
    });
    return undefined;
  }, () => {});

  // Hide everything outside the floating tree from assistive tech while
  // open (modal) or keep the combobox reference exposed (untrapped combobox).
  createEffect(() => {
    const open = rootStore().useState("open");
    const floating = rootStore().useState("floatingElement");
    const domReference = rootStore().useState("domReferenceElement");
    const isDisabled = disabled();
    const isModal = modal();
    if (isDisabled || !floating || !open) {
      return undefined;
    }
    const portalNodes = Array.from(portalContext?.portalNode?.querySelectorAll(`[${createAttribute("portal")}]`) || []);
    const ancestors = tree ? getNodeAncestors(tree.nodesRef.current, getNodeId()) : [];
    const rootAncestorComboboxDomReference = ancestors.find(node => isTypeableCombobox(node.context?.elements.domReference || null))?.context?.elements.domReference;
    const controlInsideElements = [floating, ...portalNodes, beforeGuardRef.current, afterGuardRef.current, portalContext?.beforeOutsideRef.current ?? null, portalContext?.afterOutsideRef.current ?? null, ...getResolvedInsideElements()];
    const insideElements = [...controlInsideElements, rootAncestorComboboxDomReference, resolveRef(props.previousFocusableElement), resolveRef(props.nextFocusableElement), isUntrappedTypeableCombobox() ? domReference : null].filter(x => x != null);
    const ariaHiddenCleanup = markOthers(insideElements, {
      ariaHidden: isModal || isUntrappedTypeableCombobox(),
      mark: false
    });
    const markerInsideElements = [floating, ...portalNodes].filter(x => x != null);
    const markerCleanup = markOthers(markerInsideElements);
    onCleanup(() => {
      markerCleanup();
      ariaHiddenCleanup();
    });
    return undefined;
  }, () => {});

  // Focus the initial element when the floating element opens.
  useIsoLayoutEffect(() => {
    const open = rootStore().useState("open");
    const floatingFocusElement = getFloatingFocus();
    if (!open || disabled() || !isHTMLElement(floatingFocusElement)) {
      return undefined;
    }
    const doc = ownerDocument(floatingFocusElement);
    const prevFocus = activeElement(doc);
    queueMicrotask(() => {
      const initialFocusValueOrFn = initialFocus();
      const resolvedInitialFocus = typeof initialFocusValueOrFn === "function" ? initialFocusValueOrFn(openInteractionType() || "") : initialFocusValueOrFn;
      if (resolvedInitialFocus === undefined || resolvedInitialFocus === false) {
        return;
      }
      const focusAlreadyInsideFloatingEl = contains(floatingFocusElement, prevFocus);
      if (focusAlreadyInsideFloatingEl) {
        return;
      }
      let focusableElements = null;
      const getDefaultFocusElement = () => {
        if (focusableElements == null) {
          focusableElements = getTabbableContent(floatingFocusElement);
        }
        return focusableElements[0] || floatingFocusElement;
      };
      let elToFocus;
      if (resolvedInitialFocus === true || resolvedInitialFocus === null) {
        elToFocus = getDefaultFocusElement();
      } else {
        elToFocus = resolveRef(resolvedInitialFocus);
      }
      elToFocus = elToFocus || getDefaultFocusElement();

      // `preventScroll: true` unconditionally (matches @floating-ui/react).
      // The floating element is placed by the positioner, not by the
      // browser's scroll-into-view — so focusing initial content (e.g. an
      // <input> inside a popover) must NOT scroll the page. Gating this on
      // `elToFocus === floatingFocusElement` (the old port) let inner-input
      // focus jump the page to the top while the popup was still at (0,0)
      // pre-position.
      enqueueFocus(elToFocus, {
        preventScroll: true
      });
    });
    return undefined;
  }, () => {});

  // Track return-focus targets and restore focus on unmount / close.
  useIsoLayoutEffect(() => {
    const floatingFocusElement = getFloatingFocus();
    const floating = rootStore().useState("floatingElement");
    const domReference = rootStore().useState("domReferenceElement");
    if (disabled() || !floatingFocusElement) {
      return undefined;
    }
    const doc = ownerDocument(floatingFocusElement);
    const prevFocus = activeElement(doc);
    addPreviouslyFocusedElement(prevFocus);
    function onOpenChangeLocal(details) {
      if (!details.open) {
        closeTypeRef.current = getEventType(details.nativeEvent, lastInteractionTypeRef.current);
      }
      if (details.reason === REASONS.triggerHover && details.nativeEvent.type === "mouseleave") {
        preventReturnFocusRef.current = true;
      }
      if (details.reason !== REASONS.outsidePress) {
        return;
      }
      if (details.nested) {
        preventReturnFocusRef.current = false;
      } else if (isVirtualClick(details.nativeEvent) || isVirtualPointerEvent(details.nativeEvent)) {
        preventReturnFocusRef.current = false;
      } else {
        let isPreventScrollSupported = false;
        ownerDocument(floatingFocusElement).createElement("div").focus({
          get preventScroll() {
            isPreventScrollSupported = true;
            return false;
          }
        });
        preventReturnFocusRef.current = !isPreventScrollSupported;
      }
    }
    events.on("openchange", onOpenChangeLocal);
    function getReturnElement() {
      const returnFocusValueOrFn = returnFocusProp();
      let resolvedReturnFocusValue = typeof returnFocusValueOrFn === "function" ? returnFocusValueOrFn(closeTypeRef.current) : returnFocusValueOrFn;
      if (resolvedReturnFocusValue === undefined || resolvedReturnFocusValue === false) {
        return null;
      }
      if (resolvedReturnFocusValue === null) {
        resolvedReturnFocusValue = true;
      }
      if (typeof resolvedReturnFocusValue === "boolean") {
        const el = domReference || getPreviouslyFocusedElement();
        return el && el.isConnected ? el : null;
      }
      const fallback = domReference || getPreviouslyFocusedElement();
      return resolveRef(resolvedReturnFocusValue) || fallback || null;
    }
    onCleanup(() => {
      events.off("openchange", onOpenChangeLocal);
      const activeEl = activeElement(doc);
      const insideElements = getResolvedInsideElements();
      const isFocusInsideFloatingTree = contains(floating, activeEl) || insideElements.some(element => element === activeEl || contains(element, activeEl)) || !!tree && getNodeChildren(tree.nodesRef.current, getNodeId(), false).some(node => contains(node.context?.elements.floating, activeEl));
      const returnFocusValueOrFn = returnFocusProp();
      const returnElement = getReturnElement();
      queueMicrotask(() => {
        const tabbableReturnElement = getFirstTabbableElement(returnElement);
        const hasExplicitReturnFocus = typeof returnFocusValueOrFn !== "boolean";
        if (returnFocusValueOrFn && !preventReturnFocusRef.current && isHTMLElement(tabbableReturnElement) && (!hasExplicitReturnFocus && tabbableReturnElement !== activeEl && activeEl !== doc.body ? isFocusInsideFloatingTree : true)) {
          tabbableReturnElement.focus({
            preventScroll: true
          });
        }
        preventReturnFocusRef.current = false;
      });
    });
    return undefined;
  }, () => {});

  // Safari scroll-to-bottom workaround: blur inputs inside the popup
  // before it unmounts.
  useIsoLayoutEffect(() => ({
    open: rootStore().useState("open"),
    floating: rootStore().useState("floatingElement")
  }), ({
    open,
    floating
  }) => {
    if (!isWebKit() || open || !floating) {
      return;
    }
    const activeEl = activeElement(ownerDocument(floating));
    if (!isHTMLElement(activeEl) || !isTypeableElement(activeEl)) {
      return;
    }
    if (contains(floating, activeEl)) {
      activeEl.blur();
    }
  });

  // Synchronize the `context` / `modal` value to the portal context so
  // the portal can choose whether to render its own outer guards.
  useIsoLayoutEffect(
  // Compute snapshots all reactive inputs so apply runs without
  // re-reading anything; apply returns its cleanup instead of
  // calling `onCleanup` (apply scope is unowned).
  () => ({
    open: rootStore().useState("open"),
    domReference: rootStore().useState("domReferenceElement"),
    isDisabled: disabled(),
    isModal: modal(),
    isCloseOnFocusOut: closeOnFocusOut()
  }), ({
    open,
    domReference,
    isDisabled,
    isModal,
    isCloseOnFocusOut
  }) => {
    if (isDisabled || !portalContext) {
      return undefined;
    }
    portalContext.setFocusManagerState({
      modal: isModal,
      closeOnFocusOut: isCloseOnFocusOut,
      open,
      onOpenChange: rootStore().setOpen,
      domReference
    });
    return () => {
      portalContext.setFocusManagerState(null);
    };
  });

  // Keep the floating element's tabIndex in sync.
  useIsoLayoutEffect(() => ({
    floatingFocus: getFloatingFocus(),
    isDisabled: disabled()
  }), ({
    floatingFocus,
    isDisabled
  }) => {
    if (isDisabled || !floatingFocus) {
      return undefined;
    }
    handleTabIndex(floatingFocus, orderRef);
    return () => {
      queueMicrotask(clearDisconnectedPreviouslyFocusedElements);
    };
  });

  // Guard ref callbacks — propagate internal slot + optional external ref
  // + portal-context sentinel refs.
  function setBeforeGuardRef(el) {
    beforeGuardRef.current = el;
    if (props.beforeContentFocusGuardRef) {
      props.beforeContentFocusGuardRef.current = el;
    }
    if (portalContext?.beforeInsideRef) {
      portalContext.beforeInsideRef.current = el;
    }
  }
  function setAfterGuardRef(el) {
    afterGuardRef.current = el;
    if (portalContext?.afterInsideRef) {
      portalContext.afterInsideRef.current = el;
    }
  }
  const shouldRenderGuards = () => !disabled() && (modal() ? !isUntrappedTypeableCombobox() : true) && (isInsidePortal() || modal());
  function onBeforeGuardFocus(event) {
    if (modal()) {
      const els = getTabbableContent();
      // Synchronous focus: under real-browser CDP Tab dispatch the browser
      // moves focus to the guard <span> and the test / next key event
      // reads `document.activeElement` inside the same tick. React's port
      // uses rAF here, but its tests run under `userEvent` which awaits a
      // frame automatically; native input has no such wait, so the
      // wrap-around must complete before the handler returns.
      enqueueFocus(els[els.length - 1] ?? null, {
        sync: true
      });
    } else if (portalContext?.portalNode) {
      preventReturnFocusRef.current = false;
      if (isOutsideEvent(event, portalContext.portalNode)) {
        const nextTabbable = getNextTabbable(rootStore().useState("domReferenceElement"));
        nextTabbable?.focus();
      } else {
        resolveRef(props.previousFocusableElement ?? portalContext.beforeOutsideRef)?.focus();
      }
    }
  }
  function onAfterGuardFocus(event) {
    if (modal()) {
      enqueueFocus(getTabbableContent()[0] ?? null, {
        sync: true
      });
    } else if (portalContext?.portalNode) {
      if (closeOnFocusOut()) {
        preventReturnFocusRef.current = true;
      }
      if (isOutsideEvent(event, portalContext.portalNode)) {
        const prevTabbable = getPreviousTabbable(rootStore().useState("domReferenceElement"));
        prevTabbable?.focus();
      } else {
        resolveRef(props.nextFocusableElement ?? portalContext.afterOutsideRef)?.focus();
      }
    }
  }
  return <>
			{shouldRenderGuards() && <FocusGuard data-type="inside" ref={setBeforeGuardRef} onFocus={onBeforeGuardFocus} />}
			{props.children}
			{shouldRenderGuards() && <FocusGuard data-type="inside" ref={setAfterGuardRef} onFocus={onAfterGuardFocus} />}
		</>;
}
//# sourceMappingURL=floating-focus-manager.jsx.map
