/**
 * `<Select.Popup>` — the popup container that renders the select listbox.
 * Mirrors `packages/react/src/select/popup/SelectPopup.tsx`.
 *
 * Wraps `<FloatingFocusManager>` (non-modal per React — modal semantics
 * live on the Positioner via the InternalBackdrop + scroll-lock). When a
 * `<Select.List>` child is present, the popup becomes `role="presentation"`
 * and the list element owns the listbox role; otherwise the popup itself
 * is the listbox.
 *
 * Trigger-aligned placement (`alignItemWithTrigger`) is implemented here —
 * the geometry effect measures the trigger + value + selected-item-text
 * rects and writes `positioner.style.{top,bottom,left,height,marginTop,
 * marginBottom,maxHeight}` so the selected item's text aligns with the
 * trigger's value text. The popup also wires `store.scrollUpArrowVisible` /
 * `scrollDownArrowVisible` flags (surfaced to `<Select.ScrollUpArrow>` /
 * `<Select.ScrollDownArrow>`). None of the geometry is exercisable in
 * happy-dom (0 rects); the state-machine wiring is unit-tested and the
 * math remains a browser-test gap.
 */

import { rectToClientRect } from "@floating-ui/utils";
import { dynamic } from "@solidjs/web";
import { createEffect, createMemo, omit, onCleanup, untrack } from "solid-js";
import { createChangeEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { composeRefs, userRef } from "../../internals/compose-refs.jsx";
import { ownerDocument } from "../../internals/dom/owner-document.jsx";
import { ownerWindow } from "../../internals/dom/owner-window.jsx";
import { FloatingFocusManager } from "../../internals/floating-ui/components/floating-focus-manager.jsx";
import { isWebKit } from "../../internals/floating-ui/detect-browser.jsx";
import { mergeRootProps } from "../../internals/merge-root-props.jsx";
import { resolveClassStyle } from "../../internals/resolve-class-style.jsx";
import { getMaxScrollOffset, SCROLL_EDGE_TOLERANCE_PX } from "../../internals/scroll-area-utils/scroll-edges.jsx";
import { styleDisableScrollbar } from "../../internals/scroll-area-utils/style-disable-scrollbar.jsx";
import { clamp } from "../../internals/slider-utils/clamp.jsx";
import { useAnimationFrame } from "../../internals/use-animation-frame.jsx";
import { useSelectPositionerContext } from "../positioner/select-positioner-context.jsx";
import { useSelectRootContext } from "../select-root-context.jsx";
function getMaxPopupHeight(popupStyles) {
  const maxHeightStyle = popupStyles.maxHeight || "";
  if (!maxHeightStyle.endsWith("px")) return Number.POSITIVE_INFINITY;
  const parsed = Number.parseFloat(maxHeightStyle);
  return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
}
function getMaxScrollTop(scroller) {
  return getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
}
function getScale(element) {
  // Approximate `@floating-ui/dom`'s `platform.getScale` — we don't bring
  // in the whole platform module here.
  const rect = element.getBoundingClientRect();
  const width = element.offsetWidth;
  const height = element.offsetHeight;
  const x = width > 0 ? rect.width / width : 1;
  const y = height > 0 ? rect.height / height : 1;
  return {
    x: Number.isFinite(x) && x !== 0 ? x : 1,
    y: Number.isFinite(y) && y !== 0 ? y : 1
  };
}
function normalizeSize(size, axis, scale) {
  return size / scale[axis];
}
function normalizeRect(rect, scale) {
  return rectToClientRect({
    x: normalizeSize(rect.x, "x", scale),
    y: normalizeSize(rect.y, "y", scale),
    width: normalizeSize(rect.width, "x", scale),
    height: normalizeSize(rect.height, "y", scale)
  });
}
const TRANSFORM_STYLE_RESETS = [["transform", "none"], ["scale", "1"], ["translate", "0 0"]];
function unsetTransformStyles(popupElement) {
  const {
    style
  } = popupElement;
  const originalStyles = {};
  for (const [property, value] of TRANSFORM_STYLE_RESETS) {
    originalStyles[property] = style.getPropertyValue(property);
    style.setProperty(property, value, "important");
  }
  return () => {
    for (const [property] of TRANSFORM_STYLE_RESETS) {
      const originalValue = originalStyles[property];
      if (originalValue) {
        style.setProperty(property, originalValue);
      } else {
        style.removeProperty(property);
      }
    }
  };
}
function clearPositionerStyles(element, original) {
  if (!element) return;
  element.style.top = original.top;
  element.style.left = original.left;
  element.style.right = original.right;
  element.style.height = original.height;
  element.style.bottom = original.bottom;
  element.style.minHeight = original.minHeight;
  element.style.maxHeight = original.maxHeight;
  element.style.marginTop = original.marginTop;
  element.style.marginBottom = original.marginBottom;
}

/** Mirrors React's `LIST_FUNCTIONAL_STYLES`. */
const LIST_FUNCTIONAL_STYLES = {
  overflow: "auto",
  "overscroll-behavior": "contain"
};
export function SelectPopup(props) {
  const root = useSelectRootContext();
  const positioner = useSelectPositionerContext();

  // Toggle the disable-scrollbar class imperatively via classList so the
  // spread boundary stays stable (a reactive `get class()` that flips
  // between "" and a class name at runtime breaks Solid 2's DOM binding
  // for a particular class-update path). Driven by a
  // render effect that reacts to `alignItemWithTriggerActive()` plus the
  // list-present check.
  const setPopupRef = el => {
    root.popupRef.current = el;
  };

  // Declared as a hoisted `function` (not a `const` arrow) on purpose:
  // the `internal` role/id/aria-* getters below are read by the `{...merged}`
  // spread's render-effect, whose compute runs synchronously at creation in
  // Solid 2. Under CDP's renderer scheduling a re-entrant re-flush can invoke
  // one of those getters while this body is still suspended above its own
  // declaration line — a `const` binding would throw `Cannot access 'hasList'
  // before initialization`. A function declaration hoists with its
  // body, so there is no temporal dead zone. Do NOT convert back to a const.
  function hasList() {
    return root.listElement() != null;
  }
  createEffect(() => ({
    // Track `mounted` so the effect re-fires after the popup's ref
    // callback has run (the ref fires during `insert` — before the
    // first effect apply — in Solid 2).
    mountedNow: root.mounted(),
    aligned: positioner.alignItemWithTriggerActive(),
    listPresent: hasList()
  }), ({
    aligned,
    listPresent
  }) => {
    const popup = root.popupRef.current;
    if (!popup) return;
    const shouldAdd = !listPresent && aligned;
    popup.classList.toggle(styleDisableScrollbar.className, shouldAdd);
    if (aligned && !listPresent) {
      for (const [key, value] of Object.entries(LIST_FUNCTIONAL_STYLES)) {
        popup.style.setProperty(key, String(value));
      }
    } else if (aligned && listPresent) {
      popup.style.height = "100%";
    } else {
      for (const key of Object.keys(LIST_FUNCTIONAL_STYLES)) {
        popup.style.removeProperty(key);
      }
      if (popup.style.height === "100%") {
        popup.style.removeProperty("height");
      }
    }
  });
  const state = createMemo(() => ({
    open: root.open(),
    side: positioner.side(),
    align: positioner.align(),
    transitionStatus: root.transitionStatus()
  }));

  // Note: `as`, `class`, `style`, `render`, `finalFocus` stripped at the
  // bottom via `omit` before user spread.

  // Geometry bookkeeping — `reachedMaxHeightRef` mirrors React's
  // `useRef(false)`, `initialPlacedRef` tracks whether the align effect has
  // run for the current open, and `originalPositionerStylesRef` captures
  // the positioner's styles on mount so we can restore them.
  let reachedMaxHeight = false;
  let initialPlaced = false;
  let originalPositionerStyles = null;
  const scrollArrowFrame = useAnimationFrame();

  /**
   * Probes the popup / list scroller's `scrollTop` against max-scroll and
   * writes the `scrollUp/DownArrowVisible` flags. Mirrors React's
   * `handleScrollArrowVisibility` (via the same name in `popup-store-utils`
   * on the React side). Runs after every layout change + scroll event.
   */
  const handleScrollArrowVisibility = () => {
    const popupEl = root.popupRef.current;
    if (!popupEl) return;
    const scroller = root.listElement() ?? popupEl;
    if (!scroller) return;
    const maxScrollTop = getMaxScrollTop(scroller);
    const scrollTop = scroller.scrollTop;
    const upVisible = scrollTop > SCROLL_EDGE_TOLERANCE_PX;
    const downVisible = scrollTop < maxScrollTop - SCROLL_EDGE_TOLERANCE_PX;
    if (root.scrollUpArrowVisible() !== upVisible) {
      root.setScrollUpArrowVisible(upVisible);
    }
    if (root.scrollDownArrowVisible() !== downVisible) {
      root.setScrollDownArrowVisible(downVisible);
    }
  };
  root.handleScrollArrowVisibilityRef.current = handleScrollArrowVisibility;
  onCleanup(() => {
    if (root.handleScrollArrowVisibilityRef.current === handleScrollArrowVisibility) {
      root.handleScrollArrowVisibilityRef.current = () => {};
    }
  });

  /**
   * Incremental scroll-height management for trigger-aligned popups. When
   * the user scrolls to the edge, we grow the positioner to consume the
   * remaining space (matches the item-aligned UX React ships). Only active
   * when `initialPlaced && !reachedMaxHeight && alignItemWithTriggerActive`.
   * For the non-aligned path we simply re-probe the scroll arrows.
   */
  const handleScroll = scroller => {
    const positionerElementActual = (() => {
      // The positioner element is the popup's offsetParent when `position:
      // fixed` is used for trigger-aligned. Walk up from the popup.
      const el = root.popupRef.current;
      if (!el) return null;
      const parent = el.parentElement;
      return parent;
    })();
    const positionerNode = positionerElementActual;
    if (!positionerNode || !root.popupRef.current || !initialPlaced) return;
    if (reachedMaxHeight || !positioner.alignItemWithTriggerActive()) {
      handleScrollArrowVisibility();
      return;
    }
    const isTopPositioned = positionerNode.style.top === "0px";
    const isBottomPositioned = positionerNode.style.bottom === "0px";
    if (!isTopPositioned && !isBottomPositioned) {
      handleScrollArrowVisibility();
      return;
    }
    const scale = getScale(positionerNode);
    const currentHeight = normalizeSize(positionerNode.getBoundingClientRect().height, "y", scale);
    const doc = ownerDocument(positionerNode);
    const positionerStyles = getComputedStyle(positionerNode);
    const marginTop = Number.parseFloat(positionerStyles.marginTop) || 0;
    const marginBottom = Number.parseFloat(positionerStyles.marginBottom) || 0;
    const maxPopupHeight = getMaxPopupHeight(getComputedStyle(root.popupRef.current));
    const maxAvailableHeight = Math.min(doc.documentElement.clientHeight - marginTop - marginBottom, maxPopupHeight);
    const scrollTop = scroller.scrollTop;
    const maxScrollTop = getMaxScrollTop(scroller);
    let nextPositionerHeight = 0;
    let nextScrollTop = null;
    let setReachedMax = false;
    let scrollToMax = false;
    const setHeight = height => {
      positionerNode.style.height = `${height}px`;
    };
    const handleSmallDiff = (diff, targetScrollTop) => {
      const heightDelta = clamp(diff, 0, maxAvailableHeight - currentHeight);
      if (heightDelta > 0) {
        setHeight(currentHeight + heightDelta);
      }
      scroller.scrollTop = targetScrollTop;
      if (maxAvailableHeight - (currentHeight + heightDelta) <= SCROLL_EDGE_TOLERANCE_PX) {
        reachedMaxHeight = true;
      }
      handleScrollArrowVisibility();
    };
    const diff = isTopPositioned ? maxScrollTop - scrollTop : scrollTop;
    const nextHeight = Math.min(currentHeight + diff, maxAvailableHeight);
    nextPositionerHeight = nextHeight;
    if (diff <= SCROLL_EDGE_TOLERANCE_PX) {
      handleSmallDiff(diff, isTopPositioned ? maxScrollTop : 0);
      return;
    }
    if (maxAvailableHeight - nextHeight > SCROLL_EDGE_TOLERANCE_PX) {
      if (isTopPositioned) {
        scrollToMax = true;
      } else {
        nextScrollTop = 0;
      }
    } else {
      setReachedMax = true;
      if (isBottomPositioned && scrollTop < maxScrollTop) {
        const overshoot = currentHeight + diff - maxAvailableHeight;
        nextScrollTop = scrollTop - (diff - overshoot);
      }
    }
    nextPositionerHeight = Math.ceil(nextPositionerHeight);
    if (nextPositionerHeight !== 0) {
      setHeight(nextPositionerHeight);
    }
    if (scrollToMax || nextScrollTop != null) {
      const nextMaxScrollTop = getMaxScrollTop(scroller);
      const target = scrollToMax ? nextMaxScrollTop : clamp(nextScrollTop, 0, nextMaxScrollTop);
      if (Math.abs(scroller.scrollTop - target) > SCROLL_EDGE_TOLERANCE_PX) {
        scroller.scrollTop = target;
      }
    }
    if (setReachedMax || nextPositionerHeight >= maxAvailableHeight - SCROLL_EDGE_TOLERANCE_PX) {
      reachedMaxHeight = true;
    }
    handleScrollArrowVisibility();
  };
  root.scrollHandlerRef.current = handleScroll;
  onCleanup(() => {
    if (root.scrollHandlerRef.current === handleScroll) {
      root.scrollHandlerRef.current = () => {};
    }
  });

  // Capture positioner styles once the popup + positioner element are
  // both present. Use a plain effect — we snapshot defensively; timing
  // isn't critical here (the trigger-aligned writes happen in the
  // geometry effect below).
  createEffect(() => ({
    popup: root.popupRef.current,
    // Re-run on mount transitions so the snapshot is current for the
    // next open.
    mountedNow: root.mounted()
  }), ({
    popup,
    mountedNow
  }) => {
    if (!popup || !mountedNow) return;
    const positionerElement = popup.parentElement;
    if (!positionerElement || originalPositionerStyles) return;
    originalPositionerStyles = {
      top: positionerElement.style.top || "0",
      left: positionerElement.style.left || "0",
      right: positionerElement.style.right,
      height: positionerElement.style.height,
      bottom: positionerElement.style.bottom,
      minHeight: positionerElement.style.minHeight,
      maxHeight: positionerElement.style.maxHeight,
      marginTop: positionerElement.style.marginTop,
      marginBottom: positionerElement.style.marginBottom
    };
  });

  // Reset trigger-aligned geometry when the popup is not open or the
  // feature is inactive. Matches React's `useIsoLayoutEffect` branch.
  createEffect(() => ({
    isOpen: root.open(),
    alignActive: positioner.alignItemWithTriggerActive()
  }), ({
    isOpen,
    alignActive
  }) => {
    if (isOpen || alignActive) return;
    initialPlaced = false;
    reachedMaxHeight = false;
    const popup = root.popupRef.current;
    const positionerElement = popup?.parentElement;
    if (positionerElement && originalPositionerStyles) {
      clearPositionerStyles(positionerElement, originalPositionerStyles);
    }
  });

  // Trigger-aligned placement effect. Runs as a regular effect —
  // happy-dom rects are all zero so the math is inert here, but the state-
  // machine transitions (init-placed flip, fallback-to-trigger-aligned,
  // transform-origin) are wired for browser coverage.
  createEffect(() => ({
    isOpen: root.open(),
    trigger: root.triggerElement(),
    alignActive: positioner.alignItemWithTriggerActive(),
    isPositioned: positioner.isPositioned(),
    mountedNow: root.mounted(),
    transition: root.transitionStatus(),
    listEl: root.listElement()
  }),
  // Apply scope is unowned/untracked — wrap the entire body so
  // reactive reads inside the geometry math (`root.listElement()`,
  // `root.highlightItemOnHover()`, `root.selectedIndex()`,
  // `root.activeIndex()`) plus the writebacks
  // (`root.setActiveIndex`, `positioner.setControlledAlignItemWithTrigger`)
  // don't trip `[STRICT_READ_UNTRACKED]`. The compute object above
  // already drives re-runs on every reactive input we care about.
  ({
    isOpen,
    trigger,
    alignActive,
    isPositioned,
    transition
  }) => untrack(() => {
    const popupElement = root.popupRef.current;
    if (!popupElement) return;
    const positionerElement = popupElement.parentElement;
    if (!positionerElement) return;
    if (!isOpen || !trigger) return;
    if (alignActive && !isPositioned) return;
    if (transition === "ending") return;
    if (!alignActive) {
      initialPlaced = true;
      scrollArrowFrame.request(handleScrollArrowVisibility);
      popupElement.style.removeProperty("--transform-origin");
      return;
    }
    const restoreTransformStyles = unsetTransformStyles(popupElement);
    popupElement.style.removeProperty("--transform-origin");
    try {
      const textElement = root.selectedItemTextRef.current;
      const valueElement = root.valueRef.current;
      const positionerStyles = getComputedStyle(positionerElement);
      const popupStyles = getComputedStyle(popupElement);
      const doc = ownerDocument(trigger);
      const win = ownerWindow(positionerElement);
      const scale = getScale(trigger);
      const triggerRect = normalizeRect(trigger.getBoundingClientRect(), scale);
      const positionerRect = normalizeRect(positionerElement.getBoundingClientRect(), scale);
      const triggerHeight = triggerRect.height;
      const scroller = root.listElement() ?? popupElement;
      const scrollHeight = scroller.scrollHeight;
      const borderBottom = Number.parseFloat(popupStyles.borderBottomWidth) || 0;
      const marginTop = Number.parseFloat(positionerStyles.marginTop) || 10;
      const marginBottom = Number.parseFloat(positionerStyles.marginBottom) || 10;
      const minHeight = Number.parseFloat(positionerStyles.minHeight) || 100;
      const maxPopupHeight = getMaxPopupHeight(popupStyles);
      const paddingLeft = 5;
      const paddingRight = 5;
      const triggerCollisionThreshold = 20;
      const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom;
      const viewportWidth = doc.documentElement.clientWidth;
      const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight;
      let textRect;
      let alignedLeft = triggerRect.left;
      let offsetY = 0;
      if (textElement && valueElement) {
        const valueRect = normalizeRect(valueElement.getBoundingClientRect(), scale);
        textRect = normalizeRect(textElement.getBoundingClientRect(), scale);
        alignedLeft = positionerRect.left + (valueRect.left - textRect.left);
        const valueCenterFromTriggerTop = valueRect.top - triggerRect.top + valueRect.height / 2;
        const textCenterFromPositionerTop = textRect.top - positionerRect.top + textRect.height / 2;
        offsetY = textCenterFromPositionerTop - valueCenterFromTriggerTop;
      }
      const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom;
      let height = Math.min(viewportHeight, idealHeight);
      const maxHeight = viewportHeight - marginTop - marginBottom;
      const scrollTop = idealHeight - height;
      const maxRight = viewportWidth - paddingRight;
      positionerElement.style.left = `${clamp(alignedLeft, paddingLeft, maxRight - positionerRect.width)}px`;
      positionerElement.style.height = `${height}px`;
      positionerElement.style.maxHeight = "auto";
      positionerElement.style.marginTop = `${marginTop}px`;
      positionerElement.style.marginBottom = `${marginBottom}px`;
      popupElement.style.height = "100%";
      const maxScrollTop = getMaxScrollTop(scroller);
      const isTopPositioned = scrollTop >= maxScrollTop - SCROLL_EDGE_TOLERANCE_PX;
      if (isTopPositioned) {
        height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop);
      }
      const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || Math.ceil(height) + SCROLL_EDGE_TOLERANCE_PX < Math.min(scrollHeight, minHeight);
      const isPinchZoomed = (win.visualViewport?.scale ?? 1) !== 1 && isWebKit;
      if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
        initialPlaced = true;
        if (originalPositionerStyles) {
          clearPositionerStyles(positionerElement, originalPositionerStyles);
        }
        positioner.setControlledAlignItemWithTrigger(false);
        return;
      }
      const initialHeightCalc = Math.max(minHeight, height);
      if (isTopPositioned) {
        const topOffset = Math.max(0, viewportHeight - idealHeight);
        positionerElement.style.top = positionerRect.height >= maxHeight ? "0" : `${topOffset}px`;
        positionerElement.style.height = `${height}px`;
        scroller.scrollTop = getMaxScrollTop(scroller);
      } else {
        positionerElement.style.bottom = "0";
        scroller.scrollTop = scrollTop;
      }
      if (textRect) {
        const popupTop = positionerRect.top;
        const popupHeight = positionerRect.height;
        const textCenterY = textRect.top + textRect.height / 2;
        const transformOriginY = popupHeight > 0 ? (textCenterY - popupTop) / popupHeight * 100 : 50;
        const clampedY = clamp(transformOriginY, 0, 100);
        popupElement.style.setProperty("--transform-origin", `50% ${clampedY}%`);
      }
      if (initialHeightCalc === viewportHeight || height >= maxPopupHeight) {
        reachedMaxHeight = true;
      }
      handleScrollArrowVisibility();
      if (root.highlightItemOnHover() && root.selectedIndex() === null && root.activeIndex() === null && root.listRef.current[0] != null) {
        root.setActiveIndex(0);
      }
      initialPlaced = true;
    } finally {
      restoreTransformStyles();
    }
  }));

  // Window-resize dismisses the popup while trigger-aligned (React parity).
  createEffect(() => ({
    alignActive: positioner.alignItemWithTriggerActive(),
    isOpen: root.open()
  }), ({
    alignActive,
    isOpen
  }) => {
    if (!alignActive || !isOpen) return undefined;
    const popup = root.popupRef.current;
    const win = ownerWindow(popup);
    const onResize = event => {
      root.setOpen(false, createChangeEventDetails(REASONS.windowResize, event));
    };
    win.addEventListener("resize", onResize);
    // Apply scope is unowned — return cleanup instead of `onCleanup`.
    return () => {
      win.removeEventListener("resize", onResize);
    };
  });
  const internal = {
    get role() {
      return hasList() ? "presentation" : "listbox";
    },
    get id() {
      if (hasList()) return "";
      return `${root.id() ?? ""}-list`;
    },
    get "aria-multiselectable"() {
      if (hasList()) return "";
      return root.multiple() ? "true" : "";
    },
    get "aria-orientation"() {
      return hasList() ? "" : "";
    },
    onKeyDown(event) {
      root.keyboardActiveRef.current = true;
      void event;
    },
    onMouseMove(event) {
      root.keyboardActiveRef.current = false;
      void event;
    },
    onScroll(event) {
      if (root.listElement()) return;
      const target = event.currentTarget;
      if (target) {
        root.scrollHandlerRef.current(target);
      }
    },
    // popupWithTransitionStateMapping projection.
    get "data-open"() {
      return state().open ? "" : undefined;
    },
    get "data-closed"() {
      return state().open ? undefined : "";
    },
    get "data-starting-style"() {
      return state().transitionStatus === "starting" ? "" : undefined;
    },
    get "data-ending-style"() {
      return state().transitionStatus === "ending" ? "" : undefined;
    },
    // `side`/`align` are state but not in popupWithTransitionStateMapping.
    // Default projection runs unmapped keys: data-side, data-align.
    get "data-side"() {
      return state().side;
    },
    get "data-align"() {
      return state().align;
    }
  };
  const userClassStyle = resolveClassStyle(props, () => state());
  const userProps = omit(props, "as", "class", "style", "attrs", "finalFocus");
  let merged = mergeRootProps(root.interactions.getFloatingProps(), internal);
  merged = mergeRootProps(merged, userClassStyle);
  merged = mergeRootProps(merged, userProps);
  const Tag = dynamic(() => props.as ?? "div");
  const popupElement = <Tag {...merged} ref={composeRefs(setPopupRef, userRef(props))} />;
  return <FloatingFocusManager context={root.floatingRootContext} modal={false} disabled={!root.mounted()} returnFocus={props.finalFocus ?? true} restoreFocus="popup">
			{popupElement}
		</FloatingFocusManager>;
}
//# sourceMappingURL=select-popup.jsx.map
