/**
 * Stage 4.4 port of Base UI React's `useHover` hook.
 *
 * React source:
 *   `base-ui/packages/react/src/floating-ui-react/hooks/useHover.ts`
 *
 * Single-hook variant for the common case of one reference + one
 * floating element sharing hover handlers. The React source still ships
 * this distinct from the reference / floating interaction pair (above)
 * because the one-off shape has noticeably less bookkeeping — no tree
 * `floating.closed` events, simpler close-scroll-mouseleave handling,
 * and a merged `isHoverOpen` predicate that short-circuits most of the
 * nested-tree gates.
 *
 * Consumers that need separate trigger + floating hooks (for wrapper
 * trigger modes, delegation, etc.) should use the `use-hover-*` pair
 * directly.
 *
 * ### React → Solid deviations
 *
 * Same pattern as the sibling hover hooks + `useDismiss`:
 *  - `useRef` → plain `MutableRef<T>` objects / closure `let`s.
 *  - `useValueAsRef(fn)` → read options inside handlers (no wrapping
 *    needed in Solid's non-re-rendering model).
 *  - `useStableCallback` → `createStableCallback`.
 *  - `useEffect` / `useIsoLayoutEffect` → `createEffect` /
 *    `useIsoLayoutEffect` in split-compute-apply form.
 *  - `addEventListener` + `mergeCleanups` → direct DOM-level
 *    `addEventListener` with array-based cleanup in `onCleanup`.
 *  - `isReactEvent` branches dropped: Solid already receives native
 *    events.
 *
 * ### Reason strings
 *
 * Every hover-driven `setOpen` emits `REASONS.triggerHover`. This
 * matches React's React-source literal (`'trigger-hover'`) exactly.
 */

import { isElement } from "@floating-ui/utils/dom";
import { createEffect, 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 { useTimeout } from "../../use-timeout.jsx";
import { useFloatingParentNodeId, useFloatingTree } from "../components/floating-tree.jsx";
import { getTarget, isInteractiveElement } from "../utils/element.jsx";
import { getDelay, getRestMs } from "./use-hover-shared.jsx";
import { readOption } from "../../maybe-accessor.jsx";
/**
 * Opens the floating element while hovering over the reference element, like
 * CSS `:hover`.
 * @see https://floating-ui.com/docs/useHover
 */
export function useHover(context, props = {}) {
  const store = "rootStore" in context ? context.rootStore : context;
  const {
    dataRef,
    events
  } = store.context;

  // Snapshot at hook-body-time. These options are captured in closures
  // fired by DOM events, so a body-time read is the correct semantic
  // (matches React's `useHover` where the destructure is on a memoized
  // hook input). `untrack` here just keeps `[STRICT_READ_UNTRACKED]`
  // quiet for consumers passing `get handleClose()` style accessors.
  // `enabled` is the only option that's deliberately reactive — see
  // `getEnabled` below.
  const {
    delay = 0,
    handleClose = null,
    restMs = 0,
    move = true,
    mouseOnly = false
  } = untrack(() => ({
    delay: props.delay,
    handleClose: props.handleClose,
    restMs: props.restMs,
    move: props.move,
    mouseOnly: props.mouseOnly
  }));
  const getEnabled = () => props.enabled === undefined ? true : readOption(props.enabled);
  const tree = useFloatingTree();
  const parentId = useFloatingParentNodeId();

  // React `useRef` equivalents.
  const pointerTypeRef = {
    current: undefined
  };
  const interactedInsideRef = {
    current: false
  };
  const timeout = useTimeout();
  const handlerRef = {
    current: undefined
  };
  const restTimeout = useTimeout();
  const blockMouseMoveRef = {
    current: true
  };
  const performedPointerEventsMutationRef = {
    current: false
  };
  const unbindMouseMoveRef = {
    current: () => {}
  };
  const restTimeoutPendingRef = {
    current: false
  };
  const isHoverOpen = createStableCallback(() => () => {
    const type = dataRef.current.openEvent?.type;
    return Boolean(type?.includes("mouse")) && type !== "mousedown";
  });
  const isClickLikeOpenEvent = createStableCallback(() => () => {
    if (interactedInsideRef.current) {
      return true;
    }
    const openEventType = dataRef.current.openEvent?.type;
    return openEventType === "click" || openEventType === "mousedown";
  });

  // Clear delay timeouts on close (if a close preempts an open).
  createEffect(() => undefined, () => {
    function onOpenChangeLocal(details) {
      if (!details.open) {
        timeout.clear();
        restTimeout.clear();
        blockMouseMoveRef.current = true;
        restTimeoutPendingRef.current = false;
      }
    }
    events.on("openchange", onOpenChangeLocal);
    return () => {
      events.off("openchange", onOpenChangeLocal);
    };
  });

  // When `handleClose` is set, listen for document-level `mouseleave` so
  // the floating element still closes when the cursor exits the window
  // entirely (safe-polygon alone can't detect this).
  createEffect(() => ({
    open: store.state.open,
    floatingElement: store.state.floatingElement,
    enabled: getEnabled()
  }), snapshot => {
    const {
      open,
      floatingElement,
      enabled
    } = snapshot;
    if (!enabled || !handleClose || !open) {
      return;
    }
    function onLeave(event) {
      if (isClickLikeOpenEvent()) {
        return;
      }
      if (isHoverOpen()) {
        store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event, event.currentTarget ?? undefined));
      }
    }
    const html = ownerDocument(floatingElement).documentElement;
    html.addEventListener("mouseleave", onLeave);
    return () => {
      html.removeEventListener("mouseleave", onLeave);
    };
  });
  const closeWithDelay = createStableCallback(() => (event, runElseBranch = true) => {
    const closeDelay = getDelay(delay, "close", pointerTypeRef.current);
    if (closeDelay && !handlerRef.current) {
      timeout.start(closeDelay, () => store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event)));
    } else if (runElseBranch) {
      timeout.clear();
      store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
    }
  });
  const cleanupMouseMoveHandler = createStableCallback(() => () => {
    unbindMouseMoveRef.current();
    unbindMouseMoveRef.current = () => {};
    handlerRef.current = undefined;
  });
  const clearPointerEvents = createStableCallback(() => () => {
    if (performedPointerEventsMutationRef.current) {
      const body = ownerDocument(store.state.floatingElement).body;
      body.style.pointerEvents = "";
      performedPointerEventsMutationRef.current = false;
    }
  });
  const handleInteractInside = createStableCallback(() => event => {
    const target = getTarget(event);
    if (!isInteractiveElement(target)) {
      interactedInsideRef.current = false;
      return;
    }
    interactedInsideRef.current = true;
  });

  // Main event-wiring effect. React uses a single `useEffect` keyed on the
  // reference / floating element identity; Solid re-runs when either
  // signal changes.
  createEffect(() => ({
    domReferenceElement: store.state.domReferenceElement,
    floatingElement: store.state.floatingElement,
    isOpen: store.state.open,
    enabled: getEnabled()
  }), snapshot => {
    const {
      domReferenceElement,
      floatingElement,
      isOpen,
      enabled
    } = snapshot;
    if (!enabled) {
      return;
    }
    const triggerNullable = domReferenceElement;
    if (!isElement(triggerNullable)) {
      return;
    }
    // Re-bind the narrowed element so the non-null type survives inside the
    // nested closures below (control-flow narrowing of the original `const`
    // is otherwise lost across function boundaries).
    const trigger = triggerNullable;
    let referenceHasPointer = false;
    function onReferenceMouseEnter(event) {
      referenceHasPointer = true;
      timeout.clear();
      blockMouseMoveRef.current = false;
      if (mouseOnly) {
        const pt = pointerTypeRef.current;
        if (pt !== undefined && pt !== "mouse" && pt !== "pen") {
          return;
        }
      }
      if (getRestMs(restMs) > 0 && !getDelay(delay, "open")) {
        return;
      }
      const openDelay = getDelay(delay, "open", pointerTypeRef.current);
      const triggerHtml = event.currentTarget ?? undefined;
      const domReference = store.state.domReferenceElement;
      const isOverInactiveTrigger = domReference && triggerHtml && !contains(domReference, triggerHtml);
      if (openDelay) {
        timeout.start(openDelay, () => {
          if (!store.state.open && referenceHasPointer) {
            store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerHtml));
          }
        });
      } else if (!isOpen || isOverInactiveTrigger) {
        store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerHtml));
      }
    }
    function onReferenceMouseLeave(event) {
      referenceHasPointer = false;
      if (isClickLikeOpenEvent()) {
        clearPointerEvents();
        return;
      }

      // Cancel a pending hover-open when the pointer leaves before the
      // delay elapses — even when the floating node is not mounted yet.
      if (!store.state.open) {
        timeout.clear();
      }
      unbindMouseMoveRef.current();
      const doc = ownerDocument(floatingElement);
      restTimeout.clear();
      restTimeoutPendingRef.current = false;
      const triggers = store.context.triggerElements;
      if (event.relatedTarget && triggers.hasElement(event.relatedTarget)) {
        // Moving to another trigger — keep the popup open so the
        // sibling trigger can move it.
        return;
      }
      if (handleClose && dataRef.current.floatingContext) {
        if (!store.state.open) {
          timeout.clear();
        }
        const ctx = {
          ...dataRef.current.floatingContext,
          tree,
          x: event.clientX,
          y: event.clientY,
          onClose() {
            clearPointerEvents();
            cleanupMouseMoveHandler();
            if (!isClickLikeOpenEvent()) {
              closeWithDelay(event, true);
            }
          }
        };
        const handler = handleClose(ctx);
        handlerRef.current = handler;
        doc.addEventListener("mousemove", handler);
        unbindMouseMoveRef.current = () => {
          doc.removeEventListener("mousemove", handler);
        };
        return;
      }

      // Allow interactivity without `safePolygon` on touch devices.
      // With a pointer, a short close delay is an alternative.
      if (!store.state.open) {
        return;
      }

      // Allow interactivity without `safePolygon` on touch devices.
      // With a pointer, a short close delay is an alternative.
      const shouldClose = pointerTypeRef.current === "touch" ? !contains(floatingElement, event.relatedTarget) : true;
      if (shouldClose) {
        closeWithDelay(event);
      }
    }
    function syncHoverWhileAlreadyOver() {
      if (store.state.open || timeout.isStarted()) {
        return;
      }
      let hovered = false;
      try {
        hovered = trigger.matches(":hover");
      } catch {
        hovered = false;
      }
      if (!hovered) {
        return;
      }
      onReferenceMouseEnter(new MouseEvent("mouseenter", {
        bubbles: true,
        cancelable: true
      }));
    }

    // Close the floating element after scrolling even if the pointer
    // did not move.
    // https://github.com/floating-ui/floating-ui/discussions/1692
    function onScrollMouseLeave(event) {
      if (isClickLikeOpenEvent() || !dataRef.current.floatingContext || !store.state.open) {
        return;
      }
      const triggers = store.context.triggerElements;
      if (event.relatedTarget && triggers.hasElement(event.relatedTarget)) {
        return;
      }
      if (!handleClose) {
        return;
      }
      const ctx = {
        ...dataRef.current.floatingContext,
        tree,
        x: event.clientX,
        y: event.clientY,
        onClose() {
          clearPointerEvents();
          cleanupMouseMoveHandler();
          if (!isClickLikeOpenEvent()) {
            closeWithDelay(event);
          }
        }
      };
      handleClose(ctx)(event);
    }
    function onFloatingMouseEnter() {
      timeout.clear();
      clearPointerEvents();
    }
    function onFloatingMouseLeave(event) {
      if (!isClickLikeOpenEvent()) {
        closeWithDelay(event, false);
      }
    }
    const cleanups = [];
    if (isOpen) {
      trigger.addEventListener("mouseleave", onScrollMouseLeave);
      cleanups.push(() => {
        trigger.removeEventListener("mouseleave", onScrollMouseLeave);
      });
    }
    if (move) {
      const onceHandler = evt => onReferenceMouseEnter(evt);
      trigger.addEventListener("mousemove", onceHandler, {
        once: true
      });
      cleanups.push(() => {
        trigger.removeEventListener("mousemove", onceHandler);
      });
    }
    trigger.addEventListener("mouseenter", onReferenceMouseEnter);
    trigger.addEventListener("mouseleave", onReferenceMouseLeave);
    cleanups.push(() => {
      trigger.removeEventListener("mouseenter", onReferenceMouseEnter);
      trigger.removeEventListener("mouseleave", onReferenceMouseLeave);
    });
    queueMicrotask(syncHoverWhileAlreadyOver);
    if (floatingElement) {
      floatingElement.addEventListener("mouseleave", onScrollMouseLeave);
      floatingElement.addEventListener("mouseenter", onFloatingMouseEnter);
      floatingElement.addEventListener("mouseleave", onFloatingMouseLeave);
      floatingElement.addEventListener("pointerdown", handleInteractInside, true);
      cleanups.push(() => {
        floatingElement.removeEventListener("mouseleave", onScrollMouseLeave);
        floatingElement.removeEventListener("mouseenter", onFloatingMouseEnter);
        floatingElement.removeEventListener("mouseleave", onFloatingMouseLeave);
        floatingElement.removeEventListener("pointerdown", handleInteractInside, true);
      });
    }
    return () => {
      for (const cleanup of cleanups) {
        cleanup();
      }
    };
  });

  // Block pointer-events of every element other than the reference and
  // floating while the floating element is open and has a `handleClose`
  // handler that opts in via `__options.blockPointerEvents`. Also handles
  // nested floating elements.
  // https://github.com/floating-ui/floating-ui/issues/1722
  useIsoLayoutEffect(() => ({
    open: store.state.open,
    domReferenceElement: store.state.domReferenceElement,
    floatingElement: store.state.floatingElement,
    enabled: getEnabled()
  }), snapshot => {
    const {
      open,
      domReferenceElement,
      floatingElement,
      enabled
    } = snapshot;
    if (!enabled) {
      return;
    }
    if (open && handleClose?.__options?.blockPointerEvents && isHoverOpen()) {
      performedPointerEventsMutationRef.current = true;
      const floatingEl = floatingElement;
      if (isElement(domReferenceElement) && floatingEl) {
        const body = ownerDocument(floatingElement).body;
        const ref = domReferenceElement;
        const parentFloating = tree?.nodesRef.current.find(node => node.id === parentId)?.context?.elements.floating;
        if (parentFloating) {
          parentFloating.style.pointerEvents = "";
        }
        body.style.pointerEvents = "none";
        ref.style.pointerEvents = "auto";
        floatingEl.style.pointerEvents = "auto";
        return () => {
          body.style.pointerEvents = "";
          ref.style.pointerEvents = "";
          floatingEl.style.pointerEvents = "";
        };
      }
    }
    return undefined;
  });

  // Reset shared bookkeeping when the popup closes.
  useIsoLayoutEffect(() => store.state.open, open => {
    if (!open) {
      pointerTypeRef.current = undefined;
      restTimeoutPendingRef.current = false;
      interactedInsideRef.current = false;
      cleanupMouseMoveHandler();
      clearPointerEvents();
    }
  });

  // Component-unmount cleanup — mirrors React's final `useEffect` returning
  // a function.
  onCleanup(() => {
    cleanupMouseMoveHandler();
    timeout.clear();
    restTimeout.clear();
    interactedInsideRef.current = false;
    clearPointerEvents();
  });
  const reference = {
    onPointerDown(event) {
      pointerTypeRef.current = event.pointerType;
    },
    onPointerEnter(event) {
      pointerTypeRef.current = event.pointerType;
    },
    onMouseMove(event) {
      if (!getEnabled()) {
        return;
      }
      const nativeEvent = event;
      const trigger = event.currentTarget;
      const domReference = store.state.domReferenceElement;
      const isOverInactiveTrigger = domReference && !contains(domReference, event.target);
      function handleMouseMove() {
        if (!blockMouseMoveRef.current && (!store.state.open || isOverInactiveTrigger)) {
          store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, nativeEvent, trigger));
        }
      }
      if (store.state.open && !isOverInactiveTrigger || getRestMs(restMs) === 0) {
        return;
      }
      const movementX = event.movementX;
      const movementY = event.movementY;
      // Ignore insignificant movements to account for tremors.
      if (!isOverInactiveTrigger && restTimeoutPendingRef.current && (movementX ?? 0) ** 2 + (movementY ?? 0) ** 2 < 2) {
        return;
      }
      restTimeout.clear();
      if (pointerTypeRef.current === "touch") {
        handleMouseMove();
      } else if (isOverInactiveTrigger) {
        handleMouseMove();
      } else {
        restTimeoutPendingRef.current = true;
        restTimeout.start(getRestMs(restMs), handleMouseMove);
      }
    }
  };

  // When `enabled` is a literal `false`, return empty bag. When it's
  // an accessor (or `undefined`), keep the bag and gate handlers
  // reactively. The structural check is intentional snapshot — wrap
  // in `untrack` so consumer-passed `get enabled()` getters don't
  // trip `[STRICT_READ_UNTRACKED]` from this body-time read.
  const sticksDisabled = untrack(() => typeof props.enabled !== "function" && !getEnabled());
  if (sticksDisabled) {
    return {};
  }
  return {
    reference
  };
}
//# sourceMappingURL=use-hover.jsx.map
