UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

2,116 lines • 76.3 kB
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var motionDom = require('motion-dom');
var component = require('remix/component');
var motionUtils = require('motion-utils');

class DragGesture extends motionDom.Feature {
    constructor(node) {
        super(node);
        this.removeGroupControls = motionUtils.noop;
        this.removeListeners = motionUtils.noop;
        this.controls = new motionDom.VisualElementDragControls(node);
    }
    mount() {
        // If we've been provided a DragControls for manual control over the drag gesture,
        // subscribe this component to it on mount.
        const { dragControls } = this.node.getProps();
        if (dragControls) {
            this.removeGroupControls = dragControls.subscribe(this.controls);
        }
        this.removeListeners = this.controls.addListeners() || motionUtils.noop;
    }
    update() {
        const { dragControls, drag } = this.node.getProps();
        const { dragControls: prevDragControls, drag: prevDrag } = this.node.prevProps || {};
        if (dragControls !== prevDragControls) {
            this.removeGroupControls();
            if (dragControls) {
                this.removeGroupControls = dragControls.subscribe(this.controls);
            }
        }
        /**
         * If drag is disabled mid-gesture, for instance from onDrag, end the
         * gesture as if released. This must happen before this render's
         * layout change is processed, otherwise the element is still
         * animation-blocked, so it jumps to its new layout and keeps that
         * jump as a drag offset, which release wouldn't animate away as
         * drag is now disabled.
         */
        if (prevDrag && !drag)
            this.controls.stop(undefined, undefined, prevDrag);
    }
    unmount() {
        this.removeGroupControls();
        this.removeListeners();
        /**
         * In React 19, during list reorder reconciliation, components may
         * briefly unmount and remount while the drag is still active. If we're
         * actively dragging, we should NOT end the pan session - it will
         * continue tracking pointer events via its window-level listeners.
         *
         * The pan session will be properly cleaned up when:
         * 1. The drag ends naturally (pointerup/pointercancel)
         * 2. The component is truly removed from the DOM
         */
        if (!this.controls.isDragging) {
            this.controls.endPanSession();
        }
    }
}

const asyncHandler = (handler) => (event, info) => {
    if (handler) {
        motionDom.frame.update(() => handler(event, info), false, true);
    }
};
class PanGesture extends motionDom.Feature {
    constructor() {
        super(...arguments);
        this.removePointerDownListener = motionUtils.noop;
    }
    onPointerDown(pointerDownEvent) {
        this.session = new motionDom.PanSession(pointerDownEvent, this.createPanHandlers(), {
            transformPagePoint: this.node.getTransformPagePoint(),
            // Fixes https://github.com/motiondivision/motion/issues/2270
            contextWindow: this.node.current?.ownerDocument.defaultView,
        });
    }
    createPanHandlers() {
        const { onPanSessionStart, onPanStart, onPan, onPanEnd } = this.node.getProps();
        return {
            onSessionStart: asyncHandler(onPanSessionStart),
            onStart: asyncHandler(onPanStart),
            onMove: asyncHandler(onPan),
            onEnd: (event, info) => {
                delete this.session;
                if (onPanEnd) {
                    motionDom.frame.postRender(() => onPanEnd(event, info));
                }
            },
        };
    }
    mount() {
        this.removePointerDownListener = motionDom.addPointerEvent(this.node.current, "pointerdown", (event) => this.onPointerDown(event));
    }
    update() {
        this.session?.updateHandlers(this.createPanHandlers());
    }
    unmount() {
        this.removePointerDownListener();
        this.session?.end();
    }
}

class AnimationFeature extends motionDom.Feature {
    /**
     * We dynamically generate the AnimationState manager as it contains a reference
     * to the underlying animation library. We only want to load that if we load this,
     * so people can optionally code split it out using the `m` component.
     */
    constructor(node) {
        super(node);
        node.animationState || (node.animationState = motionDom.createAnimationState(node));
    }
    updateAnimationControlsSubscription() {
        const { animate } = this.node.getProps();
        if (motionDom.isAnimationControls(animate)) {
            this.unmountControls = animate.subscribe(this.node);
        }
    }
    /**
     * Subscribe any provided AnimationControls to the component's VisualElement
     */
    mount() {
        this.updateAnimationControlsSubscription();
    }
    update() {
        if (this.node.getProps().animate !== this.node.prevProps?.animate) {
            this.updateAnimationControlsSubscription();
        }
    }
    unmount() {
        this.node.animationState.reset();
        this.unmountControls?.();
    }
}

let id = 0;
class ExitAnimationFeature extends motionDom.Feature {
    constructor() {
        super(...arguments);
        this.id = id++;
    }
    update() {
        const { presenceContext, animationState } = this.node;
        if (!presenceContext || !animationState)
            return;
        const { isPresent, onExitComplete } = presenceContext;
        /**
         * Compare presence with our own exit, not the previous presence
         * context. An element can mount already exiting, for instance when
         * Suspense reveals it in the render that removed it, and then both
         * contexts have isPresent: false (#4049).
         */
        if (isPresent === !this.exit)
            return;
        if (isPresent) {
            /**
             * When re-entering after the exit completed (element is at
             * rest), replay the enter animation from initial.
             */
            if (this.exit === true) {
                const { initial } = this.node.getProps();
                if (initial && !Array.isArray(initial)) {
                    const { transition, transitionEnd, ...target } = motionDom.resolveVariant(this.node, initial) || {};
                    for (const key in target) {
                        this.node
                            .getValue(key)
                            ?.jump(target[key]);
                    }
                }
                /**
                 * A re-entering element is no longer an initial child, so
                 * AnimatePresence's initial={false} mustn't block its enter.
                 */
                this.node.blockInitialAnimation = false;
                animationState.reset();
                animationState.animateChanges();
            }
            else {
                animationState.setActive("exit", false);
            }
            this.exit = undefined;
            return;
        }
        const exit = (this.exit = animationState.setActive("exit", true));
        if (onExitComplete) {
            exit.then(() => {
                /**
                 * An exit can resolve after the element has re-entered, e.g.
                 * when its final tick lands in the stop() of the enter.
                 */
                if (this.exit !== exit)
                    return;
                this.exit = true;
                onExitComplete(this.id);
            });
        }
    }
    mount() {
        const { register } = this.node.presenceContext || {};
        if (register) {
            this.unmount = register(this.id, this.node);
        }
        // Start the exit of an element that mounts already exiting
        this.update();
    }
    unmount() { }
}

const animations = {
    animation: {
        Feature: AnimationFeature,
    },
    exit: {
        Feature: ExitAnimationFeature,
    },
};

/**
 * Toggle `while{gesture}` and fire `on{gesture}{lifecycle}`, e.g.
 * `onHoverStart` or, with an empty lifecycle, `onTap`. A repeat fires the
 * callback but leaves `while{gesture}` active.
 */
function handleGestureEvent(node, event, gesture, lifecycle, isRepeat) {
    const { props } = node;
    const whileKey = ("while" + gesture);
    if (!isRepeat && node.animationState && props[whileKey]) {
        node.animationState.setActive(whileKey, lifecycle === "Start");
    }
    const callback = props[("on" + gesture + lifecycle)];
    if (callback) {
        motionDom.frame.postRender(() => callback(event, motionDom.extractEventInfo(event)));
    }
}
class HoverGesture extends motionDom.Feature {
    mount() {
        const { current } = this.node;
        if (!current)
            return;
        this.unmount = motionDom.hover(current, (_element, startEvent) => {
            handleGestureEvent(this.node, startEvent, "Hover", "Start");
            return (endEvent) => handleGestureEvent(this.node, endEvent, "Hover", "End");
        });
    }
    unmount() { }
}

class FocusGesture extends motionDom.Feature {
    constructor() {
        super(...arguments);
        this.isActive = false;
    }
    onFocus() {
        let isFocusVisible = false;
        /**
         * If this element doesn't match focus-visible then don't
         * apply whileHover. But, if matches throws that focus-visible
         * is not a valid selector then in that browser outline styles will be applied
         * to the element by default and we want to match that behaviour with whileFocus.
         */
        try {
            isFocusVisible = this.node.current.matches(":focus-visible");
        }
        catch (e) {
            isFocusVisible = true;
        }
        if (!isFocusVisible || !this.node.animationState)
            return;
        this.node.animationState.setActive("whileFocus", true);
        this.isActive = true;
    }
    onBlur() {
        if (!this.isActive || !this.node.animationState)
            return;
        this.node.animationState.setActive("whileFocus", false);
        this.isActive = false;
    }
    mount() {
        this.unmount = motionUtils.pipe(motionDom.addDomEvent(this.node.current, "focus", () => this.onFocus()), motionDom.addDomEvent(this.node.current, "blur", () => this.onBlur()));
    }
    unmount() { }
}

function handlePressEvent(node, event, lifecycle, isRepeat) {
    if (node.current instanceof HTMLButtonElement && node.current.disabled) {
        return;
    }
    handleGestureEvent(node, event, "Tap", lifecycle, isRepeat);
}
class PressGesture extends motionDom.Feature {
    constructor() {
        super(...arguments);
        /**
         * press() reads its options on each press, so this object is kept
         * and updated in place when props change.
         */
        this.options = {};
    }
    mount() {
        const { current } = this.node;
        if (!current)
            return;
        this.update();
        this.unmount = motionDom.press(current, (_element, startEvent) => {
            handlePressEvent(this.node, startEvent, "Start");
            return {
                onComplete: (endEvent, { success }) => handlePressEvent(this.node, endEvent, success ? "" : "Cancel"),
                onRepeat: () => handlePressEvent(this.node, startEvent, "", true),
            };
        }, this.options);
    }
    update() {
        const { options, node } = this;
        const { globalTapTarget, propagate, tap } = node.props;
        for (const key in options)
            delete options[key];
        Object.assign(options, tap, {
            useGlobalTarget: globalTapTarget,
            stopPropagation: propagate?.tap === false,
        });
    }
    unmount() { }
}

/**
 * Map an IntersectionHandler callback to an element. We only ever make one handler for one
 * element, so even though these handlers might all be triggered by different
 * observers, we can keep them in the same map.
 */
const observerCallbacks = new WeakMap();
/**
 * Multiple observers can be created for multiple element/document roots. Each with
 * different settings. So here we store dictionaries of observers to each root,
 * using serialised settings (threshold/margin) as lookup keys.
 */
const observers = new WeakMap();
const fireObserverCallback = (entry) => {
    const callback = observerCallbacks.get(entry.target);
    callback && callback(entry);
};
const fireAllObserverCallbacks = (entries) => {
    entries.forEach(fireObserverCallback);
};
function initIntersectionObserver({ root, ...options }) {
    const lookupRoot = root || document;
    /**
     * If we don't have an observer lookup map for this root, create one.
     */
    if (!observers.has(lookupRoot)) {
        observers.set(lookupRoot, {});
    }
    const rootObservers = observers.get(lookupRoot);
    const key = JSON.stringify(options);
    /**
     * If we don't have an observer for this combination of root and settings,
     * create one.
     */
    if (!rootObservers[key]) {
        rootObservers[key] = new IntersectionObserver(fireAllObserverCallbacks, { root, ...options });
    }
    return rootObservers[key];
}
function observeIntersection(element, options, callback) {
    const rootInteresectionObserver = initIntersectionObserver(options);
    observerCallbacks.set(element, callback);
    rootInteresectionObserver.observe(element);
    return () => {
        observerCallbacks.delete(element);
        rootInteresectionObserver.unobserve(element);
    };
}

const thresholdNames = {
    some: 0,
    all: 1,
};
class InViewFeature extends motionDom.Feature {
    constructor() {
        super(...arguments);
        this.hasEnteredView = false;
        this.isInView = false;
    }
    startObserver() {
        this.stopObserver?.();
        const { viewport = {} } = this.node.getProps();
        const { root, margin: rootMargin, amount = "some", once } = viewport;
        const options = {
            root: root ? root.current : undefined,
            rootMargin,
            threshold: typeof amount === "number" ? amount : thresholdNames[amount],
        };
        const onIntersectionUpdate = (entry) => {
            const { isIntersecting } = entry;
            /**
             * If there's been no change in the viewport state, early return.
             */
            if (this.isInView === isIntersecting)
                return;
            this.isInView = isIntersecting;
            /**
             * Handle hasEnteredView. If this is only meant to run once, and
             * element isn't visible, early return. Otherwise set hasEnteredView to true.
             */
            if (once && !isIntersecting && this.hasEnteredView) {
                return;
            }
            else if (isIntersecting) {
                this.hasEnteredView = true;
            }
            if (this.node.animationState) {
                this.node.animationState.setActive("whileInView", isIntersecting);
            }
            /**
             * Use the latest committed props rather than the ones in scope
             * when this observer is created
             */
            const { onViewportEnter, onViewportLeave } = this.node.getProps();
            const callback = isIntersecting ? onViewportEnter : onViewportLeave;
            callback && callback(entry);
        };
        this.stopObserver = observeIntersection(this.node.current, options, onIntersectionUpdate);
    }
    mount() {
        this.startObserver();
    }
    update() {
        if (typeof IntersectionObserver === "undefined")
            return;
        const { props, prevProps } = this.node;
        const hasOptionsChanged = ["amount", "margin", "root"].some(hasViewportOptionChanged(props, prevProps));
        if (hasOptionsChanged) {
            this.startObserver();
        }
    }
    unmount() {
        this.stopObserver?.();
        this.hasEnteredView = false;
        this.isInView = false;
    }
}
function hasViewportOptionChanged({ viewport = {} }, { viewport: prevViewport = {} } = {}) {
    return (name) => viewport[name] !== prevViewport[name];
}

const gestureAnimations = {
    inView: {
        Feature: InViewFeature,
    },
    tap: {
        Feature: PressGesture,
    },
    focus: {
        Feature: FocusGesture,
    },
    hover: {
        Feature: HoverGesture,
    },
};

const featureProps = {
    animation: ["variants", ...motionDom.variantPriorityOrder],
    exit: ["exit"],
    drag: ["drag", "dragControls"],
    focus: ["whileFocus"],
    hover: ["whileHover", "onHoverStart", "onHoverEnd"],
    tap: ["whileTap", "onTap", "onTapStart", "onTapCancel"],
    pan: ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"],
    inView: ["whileInView", "onViewportEnter", "onViewportLeave"],
    layout: ["layout", "layoutId"],
};
let isInitialized = false;
/**
 * Initialize feature definitions with isEnabled checks.
 * This must be called before any motion components are rendered.
 */
function initFeatureDefinitions() {
    if (isInitialized)
        return;
    const initialFeatureDefinitions = {};
    for (const key in featureProps) {
        initialFeatureDefinitions[key] = {
            isEnabled: (props) => featureProps[key].some((name) => !!props[name]),
        };
    }
    motionDom.setFeatureDefinitions(initialFeatureDefinitions);
    isInitialized = true;
}
/**
 * Get the current feature definitions, initializing if needed.
 */
function getInitializedFeatureDefinitions() {
    initFeatureDefinitions();
    return motionDom.getFeatureDefinitions();
}

function loadFeatures(features) {
    const featureDefinitions = getInitializedFeatureDefinitions();
    for (const key in features) {
        featureDefinitions[key] = {
            ...featureDefinitions[key],
            ...features[key],
        };
    }
    motionDom.setFeatureDefinitions(featureDefinitions);
}

/**
 * A list of all valid MotionProps.
 *
 * @privateRemarks
 * This doesn't throw if a `MotionProp` name is missing - it should.
 */
const validMotionProps = new Set([
    "animate",
    "exit",
    "variants",
    "initial",
    "style",
    "values",
    "transition",
    "transformTemplate",
    "custom",
    "inherit",
    "onBeforeLayoutMeasure",
    "onAnimationStart",
    "onAnimationComplete",
    "onUpdate",
    "onDragStart",
    "onDrag",
    "onDragEnd",
    "onMeasureDragConstraints",
    "onDirectionLock",
    "onDragTransitionEnd",
    "_dragX",
    "_dragY",
    "onHoverStart",
    "onHoverEnd",
    "onViewportEnter",
    "onViewportLeave",
    "globalTapTarget",
    "tap",
    "propagate",
    "ignoreStrict",
    "viewport",
    "pinch",
]);
/**
 * Check whether a prop name is a valid `MotionProp` key.
 *
 * @param key - Name of the property to check
 * @returns `true` is key is a valid `MotionProp`.
 *
 * @public
 */
function isValidMotionProp(key) {
    return (key.startsWith("while") ||
        (key.startsWith("drag") && key !== "draggable") ||
        key.startsWith("layout") ||
        key.startsWith("onTap") ||
        key.startsWith("onPan") ||
        key.startsWith("onLayout") ||
        validMotionProps.has(key));
}

/**
 * We keep these listed separately as we use the lowercase tag names as part
 * of the runtime bundle to detect SVG components
 */
const lowercaseSVGElements = [
    "animate",
    "circle",
    "defs",
    "desc",
    "ellipse",
    "g",
    "image",
    "line",
    "filter",
    "marker",
    "mask",
    "metadata",
    "path",
    "pattern",
    "polygon",
    "polyline",
    "rect",
    "stop",
    "switch",
    "symbol",
    "svg",
    "text",
    "tspan",
    "use",
    "view",
];

const threshold = 50;
// Pixels per millisecond (25px per frame at 60fps)
const maxSpeed = 1.5;
const overflowStyles = new Set(["auto", "scroll"]);
/**
 * Per axis and scrollable element, for the current drag: the edge it's
 * auto-scrolling towards, "start" (top/left) or "end" (bottom/right), and
 * its scroll limit when that began (Bug 1 fix). A drag in a Reorder.Board
 * scrolls on both axes, often of one element (like the page), and can
 * scroll several, so they're kept per axis and all cleared when it ends.
 */
const scrollStates = {
    x: new Map(),
    y: new Map(),
};
function resetAutoScrollState() {
    scrollStates.x.clear();
    scrollStates.y.clear();
}
function isScrollableElement(element, axis) {
    const style = getComputedStyle(element);
    const overflow = axis === "x" ? style.overflowX : style.overflowY;
    const isDocumentScroll = element === document.body ||
        element === document.documentElement;
    /**
     * An element that scrolls on one axis computes overflow auto on both
     * (e.g. a column of a board that scrolls the other way), so it must also
     * have content to scroll on this one.
     */
    return (isDocumentScroll ||
        (overflowStyles.has(overflow) &&
            (axis === "x"
                ? element.scrollWidth > element.clientWidth
                : element.scrollHeight > element.clientHeight)));
}
function findScrollableAncestor(element, axis) {
    let current = element?.parentElement;
    while (current) {
        if (isScrollableElement(current, axis)) {
            return current;
        }
        current = current.parentElement;
    }
    return null;
}
/**
 * Speed ramps up towards the edge and is capped at maxSpeed once the
 * pointer reaches or passes it. This runs from onDrag, within a frame, so
 * scale by the frame's elapsed time to scroll at the same speed whatever
 * the display's refresh rate.
 */
function scrollSpeed(distanceFromEdge) {
    const intensity = Math.min(1, 1 - distanceFromEdge / threshold);
    return maxSpeed * intensity * intensity * motionDom.frameData.delta;
}
function getScrollAmount(pointerPosition, scrollElement, axis) {
    const rect = scrollElement.getBoundingClientRect();
    const start = axis === "x" ? Math.max(0, rect.left) : Math.max(0, rect.top);
    const end = axis === "x" ? Math.min(window.innerWidth, rect.right) : Math.min(window.innerHeight, rect.bottom);
    const distanceFromStart = pointerPosition - start;
    const distanceFromEnd = end - pointerPosition;
    if (distanceFromStart < threshold) {
        return { amount: -scrollSpeed(distanceFromStart), edge: "start" };
    }
    else if (distanceFromEnd < threshold) {
        return { amount: scrollSpeed(distanceFromEnd), edge: "end" };
    }
    return { amount: 0, edge: null };
}
function autoScrollIfNeeded(groupElement, pointerPosition, axis, velocity) {
    if (!groupElement)
        return;
    const scrollableAncestor = findScrollableAncestor(groupElement, axis);
    if (!scrollableAncestor)
        return;
    // Convert pointer position from page coordinates to viewport coordinates.
    // The gesture system uses pageX/pageY but getBoundingClientRect() returns
    // viewport-relative coordinates, so we need to account for page scroll.
    const viewportPointerPosition = pointerPosition - (axis === "x" ? window.scrollX : window.scrollY);
    const { amount: scrollAmount, edge } = getScrollAmount(viewportPointerPosition, scrollableAncestor, axis);
    const states = scrollStates[axis];
    // If not in any threshold zone, clear this axis' state
    if (edge === null) {
        states.delete(scrollableAncestor);
        return;
    }
    let state = states.get(scrollableAncestor);
    const isDocumentScroll = scrollableAncestor === document.body ||
        scrollableAncestor === document.documentElement;
    // If not currently scrolling this edge, check velocity to see if we should start
    if (state?.edge !== edge) {
        // Only start scrolling if velocity is towards the edge
        const shouldStart = (edge === "start" && velocity < 0) ||
            (edge === "end" && velocity > 0);
        if (!shouldStart)
            return;
        // Activate this edge, and record the initial scroll limit
        // (prevents infinite scroll)
        state = {
            edge,
            limit: axis === "x"
                ? scrollableAncestor.scrollWidth - (isDocumentScroll ? window.innerWidth : scrollableAncestor.clientWidth)
                : scrollableAncestor.scrollHeight - (isDocumentScroll ? window.innerHeight : scrollableAncestor.clientHeight),
        };
        states.set(scrollableAncestor, state);
    }
    // Cap scrolling at initial limit (prevents infinite scroll)
    if (scrollAmount > 0) {
        const currentScroll = axis === "x"
            ? (isDocumentScroll ? window.scrollX : scrollableAncestor.scrollLeft)
            : (isDocumentScroll ? window.scrollY : scrollableAncestor.scrollTop);
        if (currentScroll >= state.limit)
            return;
    }
    // Apply scroll
    if (axis === "x") {
        if (isDocumentScroll) {
            window.scrollBy({ left: scrollAmount });
        }
        else {
            scrollableAncestor.scrollLeft += scrollAmount;
        }
    }
    else {
        if (isDocumentScroll) {
            window.scrollBy({ top: scrollAmount });
        }
        else {
            scrollableAncestor.scrollTop += scrollAmount;
        }
    }
}

function checkReorder(order, value, offset, velocity, axis, direction = "ltr") {
    const index = order.findIndex((item) => item.value === value);
    if (index === -1)
        return order;
    if (axis === "xy") {
        const { layout } = order[index];
        const point = {
            x: center(layout.x) + offset.x,
            y: center(layout.y) + offset.y,
        };
        const lines = getLines(order);
        const sourceLine = lines.find((line) => line.items.includes(order[index]));
        const targetLine = lines.reduce((closest, line) => distanceToLine(point.y, line) < distanceToLine(point.y, closest)
            ? line
            : closest);
        if (targetLine !== sourceLine) {
            return moveToLine(order, index, point.x, targetLine, direction);
        }
        const currentDistance = distanceToBox(point, layout);
        let target = -1;
        let targetDistance = currentDistance;
        order.forEach((item, targetIndex) => {
            if (targetIndex === index)
                return;
            const distance = distanceToBox(point, item.layout);
            if (distance < targetDistance) {
                target = targetIndex;
                targetDistance = distance;
            }
        });
        return target === -1
            ? order
            : motionUtils.moveItem(order, index, index + Math.sign(target - index));
    }
    if (!velocity[axis])
        return order;
    const nextOffset = velocity[axis] > 0 ? 1 : -1;
    const nextItem = order[index + nextOffset];
    if (!nextItem)
        return order;
    const item = order[index];
    const itemLayout = item.layout[axis];
    const nextLayout = nextItem.layout[axis];
    const nextItemCenter = center(nextLayout);
    if ((nextOffset === 1 && itemLayout.max + offset[axis] > nextItemCenter) ||
        (nextOffset === -1 && itemLayout.min + offset[axis] < nextItemCenter)) {
        return motionUtils.moveItem(order, index, index + nextOffset);
    }
    return order;
}
/**
 * The index a key takes `order[index]` to, or -1 if there's nowhere to go,
 * `key` being its index in Up, Left, Down, Right, Home, End. Left and Right
 * go along the order (swapped in RTL). On the "xy" axis, Up and Down go to
 * the row above or below, to the item there nearest across. Home and End
 * go to either end.
 */
function keyTarget(order, index, key, axis, isRTL) {
    const step = key > 1 !== !!(key % 2 && isRTL) ? 1 : -1;
    let target = key > 3 ? (key - 4) * (order.length - 1) : index + step;
    if (axis === "xy" && key < 4 && !(key % 2) && index > -1) {
        /**
         * Rows top to bottom as drawn, which isn't their order with
         * flex-wrap: wrap-reverse.
         */
        const lines = getLines(order).sort((a, b) => a.min - b.min);
        const line = lines[lines.findIndex(({ items }) => items.includes(order[index])) +
            step];
        const distance = (item) => Math.abs(center(item.layout.x) - center(order[index].layout.x));
        target = line
            ? order.indexOf(line.items.reduce((nearest, item) => distance(item) < distance(nearest) ? item : nearest))
            : -1;
    }
    return index > -1 && target !== index && order[target] ? target : -1;
}
const center = ({ min, max }) => motionDom.mixNumber(min, max, 0.5);
function getLines(order) {
    const lines = [];
    order.forEach((item) => {
        const { min, max } = item.layout.y;
        const line = lines[lines.length - 1];
        if (!line || min >= line.max || max <= line.min) {
            lines.push({ items: [item], min, max });
        }
        else {
            line.items.push(item);
            line.min = Math.min(line.min, min);
            line.max = Math.max(line.max, max);
        }
    });
    return lines;
}
function distanceToLine(y, line) {
    return y < line.min ? line.min - y : y > line.max ? y - line.max : 0;
}
function moveToLine(order, index, x, line, direction) {
    const remaining = order.filter((_, itemIndex) => itemIndex !== index);
    const before = line.items.find((item) => {
        const itemCenter = center(item.layout.x);
        return direction === "ltr" ? x < itemCenter : x > itemCenter;
    });
    const targetIndex = before
        ? remaining.indexOf(before)
        : remaining.indexOf(line.items[line.items.length - 1]) + 1;
    const nextOrder = [...remaining];
    nextOrder.splice(targetIndex, 0, order[index]);
    return nextOrder.every((item, itemIndex) => item === order[itemIndex])
        ? order
        : nextOrder;
}
function distanceToBox(point, box) {
    const x = Math.max(box.x.min - point.x, 0, point.x - box.x.max);
    const y = Math.max(box.y.min - point.y, 0, point.y - box.y.max);
    return x * x + y * y;
}

const isSeparated = (a, b) => a.max <= b.min || b.max <= a.min;
function detectAxis(layouts) {
    let x = false;
    let y = false;
    for (let i = 0; i < layouts.length; i++) {
        for (let j = i + 1; j < layouts.length; j++) {
            x || (x = isSeparated(layouts[i].x, layouts[j].x));
            y || (y = isSeparated(layouts[i].y, layouts[j].y));
            if (x && y)
                return "xy";
        }
    }
    return x ? "x" : "y";
}

const keys = ["ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight", "Home", "End"];
function createReorder(motion) {
    /**
     * A list of items that can be reordered by drag or keyboard, with the
     * same props as React's `Reorder.Group`.
     */
    function Group(handle) {
        const itemLayouts = new Map();
        const items = new Map();
        let active;
        let detectedAxis = "y";
        let isReordering = false;
        let element;
        let renderedValues;
        const getOrder = () => handle.props.values.flatMap((value) => {
            const layout = itemLayouts.get(value);
            return layout ? [{ value, layout }] : [];
        });
        const isRTL = () => !!element && getComputedStyle(element).direction === "rtl";
        /**
         * Write the new order of the measured items back into the slots they
         * hold in `values`, leaving unmeasured values in place.
         */
        const commit = (newOrder, moved) => {
            const { values, onReorder } = handle.props;
            let i = 0;
            const newValues = values.map((value) => itemLayouts.has(value) ? newOrder[i++].value : value);
            newValues.some((value, j) => value !== values[j]) &&
                onReorder(newValues, moved);
        };
        const getTabStop = () => {
            const { values } = handle.props;
            return values.includes(active) ? active : values[0];
        };
        const isRendered = (value) => items.get(value)?.element?.isConnected;
        /**
         * If the tab stop isn't rendered (e.g. it's virtualized), the first
         * rendered item takes its place. Items check again when removed.
         */
        const checkTabStop = () => {
            isRendered(getTabStop()) ||
                context.setActive(handle.props.values.find(isRendered));
        };
        const context = {
            axis: "y",
            hasReordered: false,
            getElement: () => element,
            items,
            isTabStop: (value) => value === getTabStop() || !handle.props.values.includes(value),
            checkTabStop,
            /**
             * Only values in `values` can be the tab stop.
             */
            setActive: (value) => {
                const tabStop = getTabStop();
                if (!handle.props.values.includes(value))
                    return;
                active = value;
                if (value !== tabStop) {
                    items.get(tabStop)?.update();
                    items.get(value)?.update();
                }
            },
            /**
             * Space grabs and drops the item, arrows along `axis` move the
             * grabbed item one place, or a row in a grid (see keyTarget), or
             * move focus that way if it isn't grabbed. Home and End go to
             * either end. Escape restores the order in `from`, the values
             * when it was grabbed.
             */
            onKeyDown: (event, item, from, grab) => {
                const { key, currentTarget } = event;
                if (event.defaultPrevented ||
                    event.target !== currentTarget ||
                    event.altKey ||
                    event.ctrlKey ||
                    event.metaKey) {
                    return;
                }
                const { axis } = context;
                const order = getOrder();
                const keyIndex = keys.indexOf(key);
                const isX = keyIndex % 2;
                if (key === " ") {
                    event.repeat ||
                        grab(currentTarget, from ? undefined : handle.props.values);
                }
                else if (key === "Escape" && from) {
                    const kept = from.flatMap((value) => order.filter((data) => data.value === value));
                    commit(order.map((data) => from.includes(data.value) ? kept.shift() : data), item);
                    grab(currentTarget);
                }
                else if (keyIndex < 0 ||
                    (keyIndex < 4 &&
                        axis !== "xy" &&
                        axis !== (isX ? "x" : "y"))) {
                    return;
                }
                else {
                    /**
                     * Focus only moves between rendered items.
                     */
                    const list = from
                        ? order
                        : order.filter(({ value }) => isRendered(value));
                    const index = list.findIndex(({ value }) => value === item);
                    const target = keyTarget(list, index, keyIndex, axis, !!isX && isRTL());
                    if (target > -1) {
                        from
                            ? commit(motionUtils.moveItem(order, index, target), item)
                            : items.get(list[target].value)?.element?.focus();
                    }
                }
                event.preventDefault();
            },
            registerItem: (value, layout) => {
                itemLayouts.set(value, layout);
                if (!handle.props.axis) {
                    const nextAxis = detectAxis(handle.props.values.flatMap((itemValue) => {
                        const itemLayout = itemLayouts.get(itemValue);
                        return itemLayout ? [itemLayout] : [];
                    }));
                    if (nextAxis !== detectedAxis) {
                        detectedAxis = nextAxis;
                        handle.update();
                    }
                }
            },
            updateOrder: (item, offset, velocity) => {
                if (isReordering)
                    return;
                const order = getOrder();
                const newOrder = checkReorder(order, item, offset, velocity, context.axis, isRTL() ? "rtl" : "ltr");
                if (order !== newOrder) {
                    isReordering = context.hasReordered = true;
                    commit(newOrder, item);
                }
            },
        };
        return () => {
            const { as = "ul", axis, values, onReorder, style, mix, children, ...props } = handle.props;
            const valuesSet = new Set(values);
            for (const map of [itemLayouts, items]) {
                map.forEach((_, value) => valuesSet.has(value) || map.delete(value));
            }
            /**
             * Only allow the next reorder once the new order has rendered.
             */
            if (values !== renderedValues) {
                renderedValues = values;
                handle.queueTask(() => (isReordering = false));
            }
            handle.queueTask(checkTabStop);
            context.axis = axis || detectedAxis;
            handle.context.set(context);
            return component.createElement(motion[as], {
                ...props,
                /**
                 * Scroll anchoring would move the page while items
                 * reorder, and with it the dragged item.
                 */
                style: { overflowAnchor: "none", ...style },
                mix: [
                    component.ref((node) => (element = node)),
                    /**
                     * The release that ends a reordering drag isn't a
                     * click. Clicks with no pointer press behind them
                     * (detail 0, e.g. keyboard) are left alone.
                     */
                    component.on("pointerdown", () => {
                        context.hasReordered = false;
                    }, true),
                    component.on("click", (event) => {
                        if (context.hasReordered && event.detail) {
                            context.hasReordered = false;
                            event.stopPropagation();
                            event.preventDefault();
                        }
                    }, true),
                    mix,
                ],
            }, children);
        };
    }
    /**
     * An item in a `Reorder.Group`, with the same props as React's
     * `Reorder.Item`.
     */
    function Item(handle) {
        /**
         * The group's values when this item was grabbed with the keyboard, for
         * Escape to restore. Set only while grabbed.
         */
        let grabbedFrom;
        const defaultX = motionDom.motionValue(0);
        const defaultY = motionDom.motionValue(0);
        let zIndex;
        const self = { update: () => handle.update() };
        const getGroup = () => handle.context.get(Group);
        /**
         * A keyboard grab is a drag without a pointer, so it shares whileDrag.
         */
        const grab = (element, from) => {
            grabbedFrom = from;
            motionDom.visualElementStore
                .get(element)
                ?.animationState?.setActive("whileDrag", !!from);
        };
        return () => {
            const { as = "li", value, style = {}, layout = true, onDrag, onDragEnd, mix, children, ...props } = handle.props;
            const group = getGroup();
            const { axis } = group;
            group.items.set(value, self);
            const x = motionDom.isMotionValue(style.x) ? style.x : defaultX;
            const y = motionDom.isMotionValue(style.y) ? style.y : defaultY;
            if (zIndex?.x !== x || zIndex.y !== y) {
                zIndex = {
                    x,
                    y,
                    value: motionDom.transformValue(() => x.get() || y.get() ? 1 : "unset"),
                };
            }
            return component.createElement(motion[as], {
                drag: axis === "xy" ? true : axis,
                tabIndex: group.isTabStop(value) ? 0 : -1,
                ...props,
                dragSnapToOrigin: true,
                style: { ...style, x, y, zIndex: zIndex.value },
                layout,
                onDrag: (event, info) => {
                    const { velocity, point } = info;
                    group.updateOrder(handle.props.value, { x: x.get(), y: y.get() }, velocity);
                    const scrollAxis = group.axis === "xy"
                        ? Math.abs(velocity.x) > Math.abs(velocity.y)
                            ? "x"
                            : "y"
                        : group.axis;
                    autoScrollIfNeeded(group.getElement() || null, point[scrollAxis], scrollAxis, velocity[scrollAxis]);
                    onDrag?.(event, info);
                },
                onDragEnd: (event, info) => {
                    // Runs a frame after release, so after the release's click
                    group.hasReordered = false;
                    resetAutoScrollState();
                    onDragEnd?.(event, info);
                },
                onLayoutMeasure: (measured) => group.registerItem(handle.props.value, measured),
                mix: [
                    mix,
                    component.ref((node, signal) => {
                        self.element = node;
                        signal.addEventListener("abort", () => queueMicrotask(group.checkTabStop));
                    }),
                    /**
                     * The item focused last, or holding focus, is the
                     * group's tab stop.
                     */
                    component.on("focusin", () => group.setActive(handle.props.value)),
                    component.on("keydown", (event) => group.onKeyDown(event, handle.props.value, grabbedFrom, grab)),
                    /**
                     * Remix blurs the item when it moves its node, and
                     * focuses it again in the same flush. So only drop
                     * it if focus is elsewhere once that's done.
                     */
                    component.on("blur", ({ currentTarget }) => queueMicrotask(() => grabbedFrom &&
                        currentTarget.ownerDocument
                            .activeElement !== currentTarget &&
                        grab(currentTarget))),
                ],
            }, children);
        };
    }
    return { Group, Item };
}

let featuresLoaded = false;
function loadRemixFeatures() {
    if (featuresLoaded)
        return;
    featuresLoaded = true;
    loadFeatures({
        ...animations,
        ...gestureAnimations,
        pan: { Feature: PanGesture },
        drag: { Feature: DragGesture },
    });
}
/**
 * Set a default `transition` and `reducedMotion` for every motion element
 * below it.
 *
 * @public
 */
function MotionConfig(handle) {
    return () => {
        const { children, ...config } = handle.props;
        handle.context.set({
            ...handle.context.get(MotionConfig),
            ...config,
        });
        return children;
    };
}
const flatten = (node) => Array.isArray(node) ? node.flat(Infinity) : [node];
/**
 * A motion component is a component, so Remix can't see the key of the
 * element it renders. Pass the key to the element, so Remix keeps the
 * element when the component is reordered.
 */
const withPresenceKey = (child) => child?.key != null && motionComponents.has(child.type)
    ? { ...child, props: { ...child.props, presenceKey: child.key } }
    : child;
/**
 * Keeps an exiting child of `AnimatePresence` until the exits of the
 * motion elements inside it finish, as React's PresenceChild.
 */
function PresenceChild(handle) {
    let exitId = 0;
    let popped = false;
    let exits = [];
    return () => {
        const { isPresent, children } = handle.props;
        if (isPresent)
            exits = [];
        const context = {
            ...handle.props,
            exits,
            popped,
        };
        handle.context.set(context);
        /**
         * Every exit registered since the child started exiting, checked
         * by the latest render only.
         */
        const id = ++exitId;
        if (isPresent) {
            popped = false;
        }
        else {
            handle.queueTask(() => {
                popped || (popped = !!context.popped);
                Promise.all([...exits]).then(() => {
                    if (id === exitId)
                        handle.props.onExitComplete();
                });
            });
        }
        return children;
    };
}
const getKey = (child, index) => child?.key ?? "__i" + index;
/**
 * Keep removed children of `AnimatePresence` until their motion elements
 * finish their `exit` animations. Also choose how entering and exiting
 * children share the screen, skip their first `initial` animation, and
 * know when exits finish.
 *
 * @public
 */
function AnimatePresence(handle) {
    let items = [];
    const exited = new Set();
    const group = {
        props: handle.props,
        isFirstRender: true,
    };
    const remove = (key) => {
        exited.add(key);
        handle.update();
    };
    return () => {
        handle.context.set(group);
        if (group.isFirstRender) {
            handle.queueTask(() => (group.isFirstRender = false));
        }
        const { mode, custom } = handle.props;
        const present = flatten(handle.props.children).filter((child) => child != null && child !== false && child !== true);
        const presentKeys = new Map(present.map((child, i) => [getKey(child, i), child]));
        /**
         * Merge the present children with the exiting ones, which keep
         * their place.
         */
        const next = present.map((child, i) => ({
            key: getKey(child, i),
            child,
            isPresent: true,
        }));
        items.forEach((item, i) => {
            if (presentKeys.has(item.key) || exited.has(item.key))
                return;
            next.splice(Math.min(i, next.length), 0, {
                ...item,
                isPresent: false,
            });
        });
        const hadExited = exited.size > 0;
        exited.clear();
        items = next;
        /**
         * Once every exit finishes. Not when an exiting child comes back.
         */
        const isExiting = next.some((item) => !item.isPresent);
        if (hadExited && !isExiting) {
            handle.queueTask(() => handle.props.onExitComplete?.());
        }
        /**
         * In "wait" mode, render the new children once the old ones
         * have exited.
         */
        const shown = mode === "wait" && isExiting
            ? next.filter((item) => !item.isPresent)
            : next;
        return shown.map(({ key, child, isPresent }) => component.createElement(PresenceChild, {
            key,
            isPresent,
            custom,
            popLayout: mode === "popLayout",
            anchorX: handle.props.anchorX,
            anchorY: handle.props.anchorY,
            onExitComplete: () => remove(key),
        }, withPresenceKey(child)));
    };
}
/**
 * Inside `AnimatePresence`: keep the exiting child until `exit` resolves,
 * like React's `safeToRemove`. Call it in each render while `isPresent()`
 * is `false`.
 *
 * @public
 */
function holdPresence(handle, exit) {
    const context = handle.context.get(PresenceChild);
    context && !context.isPresent && context.exits.push(exit);
}
/**
 * Inside `AnimatePresence`: whether the component is present, or exiting.
 *
 * @public
 */
function isPresent(handle) {
    const context = handle.context.get(PresenceChild);
    return context?.isPresent ?? true;
}
/**
 * Inside `AnimatePresence`: the `custom` value of `AnimatePresence`, while
 * the component exits.
 *
 * @public
 */
function getPresenceData(handle) {
    return handle.context.get(PresenceChild)?.custom;
}
/**
 * Give the `layoutId`s inside an `id`, so separate groups don't animate
 * between each other.
 *
 * @public
 */
function LayoutGroup(handle) {
    return () => {
        const parent = handle.context.get(LayoutGroup)?.id;
        const { id } = handle.props;
        handle.context.set({
            id: parent && id ? parent + "-" + id : id || parent,
        });
        return handle.props.children;
    };
}
/**
 * Passes variant labels from a motion component to the motion components
 * inside it, so their server render starts in the inherited variant.
 */
function VariantProvider(handle) {
    return () => {
        handle.context.set(handle.props.value);
        return handle.props.children;
    };
}
const isLabels = (value) => typeof value === "string" || Array.isArray(value);
function getVariantContext(props, parent) {
    if (!motionDom.isControllingVariants(props)) {
        return props.inherit === false ? undefined : parent;
    }
    return {
        initial: props.initial === false || isLabels(props.initial)
            ? props.initial
            : undefined,
        animate: isLabels(props.animate) ? props.animate : undefined,
    };
}
const isSVGElement = (tag) => !tag.includes("-") &&
    (lowercaseSVGElements.includes(tag) || /[A-Z]/u.test(tag));
/**
 * The values a motion element renders before it animates, as React's
 * `makeLatestValues`.
 */
function makeLatestValues(props, context, isSVG, isInitialBlocked) {
    const values = {};
    const motionValues = (isSVG ? motionDom.scrapeSVGMotionValuesFromProps : motionDom.scrapeHTMLMotionValuesFromProps)(props, {}, undefined);
    for (const key in motionValues) {
        values[key] = motionDom.resolveMotionValue(motionValues[key]);
    }
    let { initial, animate } = props;
    if (context &&
        motionDom.isVariantNode(props) &&
        !motionDom.isControllingVariants(props) &&
        props.inherit !== false) {
        if (initial === undefined)
            initial = context.initial;
        if (animate === undefined)
            animate = context.animate;
    }
    const isBlocked = isInitialBlocked || initial === false;
    const toSet = isBlocked ? animate : initial;
    if (!toSet || typeof toSet === "boolean")
        return values;
    for (const definition of Array.isArray(toSet) ? toSet : [toSet]) {
        const resolved = motionDom.resolveVariantFromProps(props, definition);
        if (!resolved)
            continue;
        const { transitionEnd, transition, ...target } = resolved;
        for (const key in target) {
            let value = target[key];
            if (Array.isArray(value)) {
                value = value[isBlocked ? value.length - 1 : 0];
            }
            if (value !== null)
                values[key] = value;
        }
        Object.assign(values, transitionEnd);
    }
    return values;
}
/**
 * Split `style` into what Remix renders and what Motion owns: motion
 * values and transforms like `x`, which the VisualElement renders.
 */
function splitStyle(style, props) {
    const css = {};
    for (const key in style) {
        const value = style[key];
        if (!motionDom.isMotionValue(value) && !motionDom.isForcedMotionValue(key, props)) {
            css[key] = value;
        }
    }
    return css;
}
const createRenderState = () => ({
    style: {},
    vars: {},
    transform: {},
    transformOrigin: {},
    attrs: {},
});
/**
 * The style, and attributes for SVG, that render `latestValues`.
 */
function renderValues(latestValues, props, tag, isSVG) {
    const state = createRenderState();
    if (isSVG) {
        motionDom.buildSVGAttrs(state, latestValues, motionDom.isSVGTag(tag), props.transformTemplate, props.style);
    }
    else {
        motionDom.buildHTMLStyles(state, latestValues, props.transformTemplate);
    }
    return { ...state.attrs, style: { ...state.vars, ...state.style } };
}
/**
 * Run each callback even if an earlier one throws, and report the error.
 */
const run = (callback) => {
    try {
        callback();
    }
    catch (error) {
        queueMicrotask(() => {
            throw error;
        });
    }
};
/**
 * Elements inserted in one commit are mounted together on the next
 * microtask, so every parent exists before its children look it up.
 * Remix inserts children before their parents.
 */
let insertQueue;
function queueInsert(create, mount, animate) {
    if (!insertQueue) {
        insertQueue = [];
        queueMicrotask(flushInserts);
    }
    insertQueue.push([create, mount, animate]);
}
/**
 * Mount the elements inserted in this commit. Also runs before a layout
 * snapshot, in case they update again before the microtask.
 */
function flushInserts() {
    const queue = insertQueue;
    if (!queue)
        return;
    insertQueue = undefined;
    // Parents first, so children join their variant tree...
    for (let i = queue.length - 1; i >= 0; i--)
        run(queue[i][0]);
    for (let i = queue.length - 1; i >= 0; i--)
        run(queue[i][1]);
    // ...and children animate first, as with React's effects.
    for (const [, , animate] of queue)
        run(animate);
}
/**
 * Updates queued in one commit: every element gets its new props before
 * any of them animates, as React's insertion effects run before its
 * effects. So a variant change from a parent animates its children with
 * their new transitions.
 */
let updateQueue;
function queueUpdate(update, animate) {
    if (!updateQueue) {
        updateQueue = [];
        queueMicrotask(() => {
            const queue = updateQueue;
            updateQueue = undefined;
            for (const [update] of queue)
                run(update);
            // Parents render first, so animate in reverse: children first.
            for (let i = queue.length - 1; i >= 0; i--)
                run(queue[i][1]);
        });
    }
    updateQueue.push([update, animate]);
}
/**
 * Elements created and not yet mounted, so children inserted in the same
 * commit can find their parent.
 */
const pendingElements = /*@__PURE__*/ new WeakMap();
function findParent(element) {
    for (let node = element.parentElement; node; node = node.parentElement) {
        const parent = pendingElements.get(node) || motionDom.visualElementStore.get(node);
        if (parent)
            return parent;
    }
    return undefined;
}
/**
 * Remix only tells the root of a removed subtree that it's leaving, so
 * the root starts the exits of the motion elements inside it, as
 * AnimatePresence does in React, and waits for all of them.
 */
const presences = /*@__PURE__*/ new WeakMap();
/**
 * Take an exiting element out of the layout, where it is now, so the
 * elements around it move at once. Returns a function that puts it back.
 */
function measurePop(node) {
    if (!(node instanceof HTMLElement))
        return;
    const parent = node.offsetParent;
    const { offsetTop: top, offsetLeft: left } = node;
    const { width, height } = getComputedStyle(node);
    const w = parseFloat(width);
    const h = parseFloat(height);
    return {
        top,
        left,
        right: (parent?.offsetWidth || 0) - w - left,
        bottom: (parent?.offsetHeight || 0) - h - top,
        width,
        height,
    };
}
function popLayout(node, box = measurePop(node), anchorX = "left", anchorY = "top") {
    if (!(node instanceof HTMLElement) || !box)
        return;
    const { style } = node;
    const popped = {
        position: "absolute",
        width: box.width,
        height: box.height,
        [anchorX]: box[anchorX] + "px",
        [anchorY]: box[anchorY] + "px",
    };
    const previous = {};
    for (const key in popped)
        previous[key] = style[key];
    Object.assign(style, popped);
    return () => Object.assign(style, previous);
}
const getLayoutNode = (node) => motionDom.acquireProjectionNode(node)[0];
/**
 * layoutId elements exiting within AnimatePresence, and how to finish
 * their exit. A layout snapshot resets node options, so they're marked
 * again after each one.
 */
const exitingLayouts = /*@__PURE__*/ new Map();
function markLayoutExit(node) {
    const projection = getLayoutNode(node);
    projection.isPresent = false;
    /**
     * Nodes created in this snapshot promote in document order, so an
     * exiting node can resume from the one that takes over from it.
     */
    projection.resumeFrom = undefined;
    projection.setOptions({ onExitComplete: exitingLayouts.get(node) });
    return projection;
}
/**
 * Layout animations: every host with `layout` or `layoutId` that is about
 * to update shares one animateLayout() for the whole document. It must
 * snapshot synchronously in `beforeUpdate`, as Remix changes the DOM
 * straight after, and finishes when the first host commits.
 */
let layoutPass;
function snapshotLayout() {
    if (layoutPass)
        return;
    flushInserts();
    let commit;
    const committed = new Promise((resolve) => {
        commit = () => {
            // A late fallback mustn't end a newer pass.
            if (layoutPass?.commit === commit)
                layoutPass = undefined;
            resolve();
        };
    });
    layoutPass = {
        builder: motionDom.animateLayout(document.documentElement, () => committed),
        commit,
    };
    motionDom.flushLayoutSnapshots();
    exitingLayouts.forEach((_, node) => markLayoutExit(node));
    /**
     * If the update is abandoned, don't block the next one.
     */
    requestAnimationFrame(commit);
}
/**
 * Animate to the latest props. Then forget which children entered with
 * this render, so the next children's `delayChildren` stagger starts
 * from 0, as in React.
 */
function animateChanges(visualElement) {
    if (!visualElement)
        return;
    visualElement.animationState?.animateChanges();
    visualElement.enteringChildren = undefined;
}
let presenceId = 0;
const motionMixin = /*@__PURE__*/ component.createMixin((handle, tag) => {
    const isSVG = isSVGElement(tag);
    let props = {};
    let element;
    let visualElement;
    let releaseProjection;
    let initialValues;
    let presence = null;
    let isExiting = false;
    let isInitialBlocked;
    const id = String(presenceId++);
    const getConfig = () => handle.context.get(MotionConfig);
    const getVariantParent = () => handle.context.get(VariantProvider);
    const getGroup = () => handle.context.get(AnimatePresence);
    const getPresenceChild = () => handle.context.get(PresenceChild);
    /**
     * Inside AnimatePresence: the exit this element started.
     */
    let exitDone;
    let exitResolvers = [];
    let pop;
    let popBox;
    let restoreLayout;
    const getProps = () => {
        const transition = props.transition ?? getConfig()?.transition;
        return transition ? { ...props, transition } : props;
    };
    const getLayoutId = () => {
        const group = handle.context.get(LayoutGroup)?.id;
        const { layoutId } = props;
        return group && layoutId ? group + "-" + layoutId : layoutId;
    };
    const setPresence = (isPresent, onExitComplete) => {
        const custom = getPresenceChild()?.custom;
        presence = {
            id,
            isPresent,
            register: () => () => { },
            onExitComplete,
            custom: custom === undefined ? props.custom : custom,
        };
        visualElement.update(getProps(), presence);
        visualElement.updateFeatures();
    };
    /**
     * Drag measures layout, so it needs a projection node before its
     * feature mounts, including when drag is turned on later. A
     * layoutId element joins its stack when it mounts, as in React.
     */
    const acquireProjection = (node) => {
        if (!props.drag && !getLayoutId())
            return;
        releaseProjection || (releaseProjection = motionDom.acquireProjectionNode(node)[1]);
        props.drag &&
            getLayoutNode(node).setOptions({ alwaysMeasureLayout: true });
    };
    /**
     * Exit within AnimatePresence: animate `exit`, and hand a layoutId
     * over to the element that replaces it, as React's exit does.
     */
    const startExit = (layoutId) => {
        const current = visualElement;
        const exits = [];
        if (props.exit) {
            isExiting = true;
            exits.push(new Promise((resolve) => {
                exitResolvers.push(resolve);
                handle.queueTask(() => {
                    if (visualElement === current && isExiting) {
                        setPresence(false, resolve);
                    }
                    else
                        resolve();
                });
            }));
        }
        const node = element;
        if (layoutId) {
            exits.push(new Promise((resolve) => {
                exitingLayouts.set(node, () => {
                    exitingLayouts.delete(node);
                    resolve();
                });
                /**
                 * Measure the layout now, before the DOM changes,
                 * and hand over as React does before its commit.
                 */
                snapshotLayout();
                const projection = markLayoutExit(node);
                /**
                 * If an element replaces this one, it crossfades
                 * from it and calls onExitComplete.
                 */
                projection.relegate() ||
                    motionDom.frame.postRender(() => {
                        const stack = projection.getStack();
                        if (!stack || stack.lead === projection) {
                            exitingLayouts.get(node)?.();
                        }
                    });
            }));
        }
        // In case this element didn't update in the commit.
        handle.queueTask(beforeLayout);
        if (layoutId)
            handle.queueTask(() => layoutPass?.commit());
        return Promise.all(exits).then(() => { });
    };
    handle.addEventListener("insert", ({ node }) => {
        element = node;
        loadRemixFeatures();
        presences.set(node, {
            exit: () => {
                if (!props.exit || !visualElement || isExiting)
                    return;
                isExiting = true;
                return new Promise((resolve) => setPresence(false, resolve));
            },
            enter: () => {
                if (!isExiting || !visualElement)
                    return;
                isExiting = false;
                setPresence(true);
            },
        });
        queueInsert(() => {
            /**
             * Removed before it mounted.
             */
            if (element !== node)
                return;
            const nodeProps = getProps();
            const VisualElement = isSVG
                ? motionDom.SVGVisualElement
                : motionDom.HTMLVisualElement;
            visualElement = new VisualElement({
                parent: findParent(node),
                props: nodeProps,
                presenceContext: presence,
                blockInitialAnimation: isInitialBlocked,
                reducedMotionConfig: getConfig()?.reducedMotion,
                visualState: {
                    latestValues: makeLatestValues(nodeProps, getVariantParent(), isSVG, isInitialBlocked),
                    renderState: createRenderState(),
                },
            });
            pendingElements.set(node, visualElement);
        }, () => {
            pendingElements.delete(node);
            if (element !== node || !visualElement)
                return;
            visualElement.mount(node);
            /**
             * The element rendered its first values. Render the
             * latest, in case they changed before it mounted.
             */
            visualElement.scheduleRenderMicrotask();
            acquireProjection(node);
            visualElement.updateFeatures();
        }, () => {
            if (element === node)
                animateChanges(visualElement);
        });
    });
    handle.addEventListener("beforeRemove", (event) => {
        const node = element;
        if (!node || isExiting || getPresenceChild())
            return;
        const leaving = [node, ...node.querySelectorAll("*")]
            .map((el) => presences.get(el))
            .filter(Boolean);
        const exited = leaving
            .map((presence) => presence.exit())
            .filter(Boolean);
        if (!exited.length)
            return;
        let cancel;
        const done = new Promise((resolve) => {
            cancel = resolve;
            Promise.all(exited).then(() => resolve());
        });
        event.persistNode((signal) => {
            const reclaim = () => {
                leaving.forEach((presence) => presence.enter());
                cancel();
            };
            if (signal.aborted)
                reclaim();
            signal.addEventListener("abort", reclaim);
            return done;
        });
    });
    handle.addEventListener("remove", () => {
        if (element) {
            presences.delete(element);
            // Removed by its own component mid-exit: its exits are done.
            exitingLayouts.get(element)?.();
        }
        exitResolvers.forEach((resolve) => resolve());
        exitResolvers = [];
        releaseProjection?.();
        visualElement?.unmount();
        releaseProjection = visualElement = element = undefined;
        presence = null;
    });
    /**
     * After the DOM changes, before the layout animation measures it:
     * take an exiting element out of the layout.
     */
    const beforeLayout = () => {
        pop?.();
        pop = undefined;
    };
    handle.addEventListener("beforeUpdate", ({ node }) => {
        /**
         * Where this element is before the DOM changes, in case it
         * exits in this update and leaves the layout.
         */
        if (getPresenceChild()?.popLayout && !exitDone) {
            popBox = measurePop(node);
        }
        const hasLayout = props.layout || props.layoutId;
        if (hasLayout)
            snapshotLayout();
        else if (!getPresenceChild()?.popLayout)
            return;
        /**
         * Set the options once this render's props have arrived, after
         * the snapshot has reset them, and before the layout animation
         * starts.
         */
        queueMicrotask(() => {
            beforeLayout();
            // Removed in this update.
            if (!hasLayout || element !== node)
                return;
            /**
             * The layout animation's node, which isn't always the
             * VisualElement's: animateLayout() can adopt the element
             * before it mounts.
             */
            getLayoutNode(node).setOptions({
                transition: getProps().transition,
                animationType: typeof props.layout === "string"
                    ? props.layout
                    : "both",
                layoutAnchor: props.layoutAnchor,
                crossfade: props.layoutCrossfade,
            });
        });
    });
    handle.addEventListener("commit", () => layoutPass?.commit());
    return (nextProps, hostProps) => {
        props = {
            ...nextProps,
            style: hostProps.style ?? nextProps.style,
        };
        /**
         * Motion renders motion values passed as attributes, like
         * <motion.circle cx={cx} />, so Remix never sees them.
         */
        const motionAttrs = {};
        for (const key in hostProps) {
            const value = hostProps[key];
            if (key !== "style" && motionDom.isMotionValue(value)) {
                props[key] = value;
                motionAttrs[key] = undefined;
            }
        }
        const { layout } = props;
        const layoutId = getLayoutId();
        const presenceChild = getPresenceChild();
        if (presenceChild && visualElement && element) {
            if (!presenceChild.isPresent) {
                if (!exitDone) {
                    exitDone = startExit(layoutId);
                    /**
                     * The outermost motion element of the child leaves
                     * the layout. Parents render before children.
                     */
                    if (presenceChild.popLayout && !presenceChild.popped) {
                        presenceChild.popped = true;
                        const node = element;
                        const { anchorX, anchorY } = presenceChild;
                        const box = popBox;
                        pop = () => (restoreLayout = popLayout(node, box, anchorX, anchorY));
                    }
                }
                presenceChild.exits.push(exitDone);
            }
            else if (exitDone) {
                exitDone = pop = undefined;
                exitResolvers = [];
                restoreLayout?.();
                restoreLayout = undefined;
                if (isExiting) {
                    isExiting = false;
                    setPresence(true);
                }
                if (exitingLayouts.delete(element)) {
                    const projection = getLayoutNode(element);
                    projection.isPresent = true;
                    projection.setOptions({ onExitComplete: undefined });
                    projection.promote();
                }
            }
        }
        /**
         * A layoutId element inserted during a layout pass animates
         * from the element it replaces, with its own transition.
         */
        if (layoutId && layoutPass && !element) {
            layoutPass.builder.shared(layoutId, getProps().transition || {});
        }
        if (visualElement) {
            const current = visualElement;
            handle.queueTask(() => queueUpdate(() => {
                if (visualElement !== current)
                    return;
                current.update(getProps(), presence);
                acquireProjection(element);
                current.updateFeatures();
            }, () => visualElement === current && animateChanges(current)));
        }
        /**
         * Render the values Motion starts from, on the server too. Keep
         * rendering the same values on later renders, so Remix's
         * per-property diff never overwrites what Motion animated.
         */
        if (!initialValues) {
            const group = getGroup();
            isInitialBlocked =
                group?.isFirstRender && group.props.initial === false;
            initialValues = renderValues(makeLatestValues(getProps(), getVariantParent(), isSVG, isInitialBlocked), props, tag, isSVG);
        }
        const { style: initialStyle, ...attrs } = initialValues;
        const patch = { ...motionAttrs, ...attrs };
        const style = { ...splitStyle(props.style, props), ...initialStyle };
        /**
         * As React: no ghost image, text selection or scrolling in the
         * drag direction, and tap gestures are keyboard accessible.
         */
        if (!isSVG) {
            const { drag } = props;
            if (drag && props.dragListener !== false) {
                patch.draggable = false;
                style.userSelect =
                    style.WebkitUserSelect =
                        style.WebkitTouchCallout =
                            "none";
                style.touchAction =
                    drag === true ? "none" : `pan-${drag === "x" ? "y" : "x"}`;
            }
            if (hostProps.tabIndex === undefined &&
                (props.onTap || props.onTapStart || props.whileTap)) {
                patch.tabIndex = 0;
            }
        }
        if (Object.keys(style).length)
            patch.style = style;
        if (layout)
            patch["data-layout"] = layout === true ? "" : layout;
        if (layoutId)
            patch["data-layout-id"] = layoutId;
        return component.createElement(handle.element, { ...hostProps, ...patch });
    };
});
const motionComponents = /*@__PURE__*/ new WeakSet();
function createMotionComponent(tag) {
    const component$1 = (handle) => {
        let text;
        return () => {
            let { children, mix, presenceKey, ...rest } = handle.props;
            const motionProps = {};
            const hostProps = {};
            for (const key in rest) {
                if (key === "style" || !isValidMotionProp(key)) {
                    hostProps[key] = rest[key];
                }
                if (isValidMotionProp(key))
                    motionProps[key] = rest[key];
            }
            /**
             * A motion value child renders its first value. Motion then
             * updates the text, so keep rendering the same text.
             */
            if (motionDom.isMotionValue(children)) {
                motionProps.children = children;
                if (text?.value !== children) {
                    text = { value: children, initial: String(children.get()) };
                }
                children = text.initial;
            }
            const parent = handle.context.get(VariantProvider);
            return component.createElement(tag, {
                ...hostProps,
                ...(presenceKey != null && { key: presenceKey }),
                mix: [motionMixin(motionProps), mix],
            }, component.createElement(VariantProvider, { value: getVariantContext(motionProps, parent) }, children));
        };
    };
    motionComponents.add(component$1);
    return component$1;
}
const components = /*@__PURE__*/ new Map();
/**
 * Animate elements with Motion in Remix 3, with the same props as React's
 * `<motion.div>`:
 *
 * ```tsx
 * <motion.ul initial="hidden" animate="visible" variants={list}>
 *     {items.map((item) => (
 *         <motion.li key={item.id} variants={item} exit={{ opacity: 0 }} />
 *     ))}
 * </motion.ul>
 * ```
 *
 * `initial` is rendered on the server. `exit` keeps an element, and
 * everything inside it, in the DOM until its exit animation finishes.
 *
 * `motion(props)` is the same as a mixin for any element, as used by the
 * components.
 *
 * Experimental: Remix 3's component API is new, so this API can change
 * in a minor release.
 *
 * @public
 */
const motion = /*@__PURE__*/ new Proxy(motionMixin, {
    get(target, key, receiver) {
        if (typeof key !== "string" || key in target) {
            return Reflect.get(target, key, receiver);
        }
        let component = components.get(key);
        if (!component) {
            component = createMotionComponent(key);
            components.set(key, component);
        }
        return component;
    },
});
const scopeMixin = /*@__PURE__*/ component.createMixin((handle) => {
    let setup = () => { };
    let scopeOptions;
    let scope;
    handle.addEventListener("insert", ({ node }) => {
        scope = motionDom.scopeMotion(node, (s) => setup(s), scopeOptions);
    });
    handle.addEventListener("remove", () => {
        scope?.cancel();
        scope = undefined;
    });
    return (nextSetup, nextOptions) => {
        setup = nextSetup;
        scopeOptions = nextOptions;
        return handle.element;
    };
}, { ssr: false });
/**
 * The `mix` form of vanilla `scopeMotion()`: run `setup` in a scope rooted
 * at this element, so selectors resolve inside it. Everything it starts is
 * cancelled, and inline styles restored, when the element is removed.
 *
 * ```tsx
 * <ul mix={scopeMotion(() => {
 *     animate("li", { opacity: [0, 1] }, { delay: stagger(0.05) })
 * })} />
 * ```
 *
 * @public
 */
const scopeMotion = scopeMixin;
/**
 * Drag, or grab with Space and move with the arrow keys, to reorder a
 * list, with the same props as React's `Reorder`:
 *
 * ```tsx
 * <Reorder.Group values={items} onReorder={(next) => {
 *     items = next
 *     handle.update()
 * }}>
 *     {items.map((item) => (
 *         <Reorder.Item key={item} value={item}>{item}</Reorder.Item>
 *     ))}
 * </Reorder.Group>
 * ```
 *
 * @public
 */
const Reorder = /*@__PURE__*/ createReorder(motion);

exports.AnimatePresence = AnimatePresence;
exports.LayoutGroup = LayoutGroup;
exports.MotionConfig = MotionConfig;
exports.Reorder = Reorder;
exports.getPresenceData = getPresenceData;
exports.holdPresence = holdPresence;
exports.isPresent = isPresent;
exports.motion = motion;
exports.scopeMotion = scopeMotion;
//# sourceMappingURL=remix.js.map