UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1,910 lines • 72.9 kB
'use strict';

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

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

function isDOMKeyframes(keyframes) {
    return typeof keyframes === "object" && !Array.isArray(keyframes);
}

function resolveSubjects(subject, keyframes, scope, selectorCache) {
    if (subject == null) {
        return [];
    }
    if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
        return motionDom.resolveElements(subject, scope, selectorCache);
    }
    else if (subject instanceof NodeList) {
        return Array.from(subject);
    }
    else if (Array.isArray(subject)) {
        return subject.filter((s) => s != null);
    }
    else {
        return [subject];
    }
}

function calculateRepeatDuration(duration, repeat, repeatDelay) {
    return duration * (repeat + 1) + repeatDelay * repeat;
}

/**
 * The time of a label, or a label with an offset like "label+1" or
 * "label-0.5". An exact label name wins over an offset.
 */
function calcLabelTime(position, labels) {
    if (labels.has(position))
        return labels.get(position);
    const [, name, offset] = position.match(/^(.+?)([+-][\d.]+)$/) || [];
    return labels.has(name)
        ? Math.max(0, labels.get(name) + parseFloat(offset))
        : undefined;
}
/**
 * Given a absolute or relative time definition and current/prev time state of the sequence,
 * calculate an absolute time for the next keyframes.
 */
function calcNextTime(current, next, prev, labels) {
    if (typeof next === "number") {
        return next;
    }
    else if (next.startsWith("-") || next.startsWith("+")) {
        return Math.max(0, current + parseFloat(next));
    }
    else if (next === "<") {
        return prev;
    }
    else if (next.startsWith("<")) {
        return Math.max(0, prev + parseFloat(next.slice(1)));
    }
    else {
        return calcLabelTime(next, labels) ?? current;
    }
}

function eraseKeyframes(sequence, startTime, endTime) {
    for (let i = 0; i < sequence.length; i++) {
        const keyframe = sequence[i];
        if (keyframe.at > startTime && keyframe.at < endTime) {
            motionUtils.removeItem(sequence, keyframe);
            // If we remove this item we have to push the pointer back one
            i--;
        }
    }
}
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
    /**
     * Erase every existing value between currentTime and targetTime,
     * this will essentially splice this timeline into any currently
     * defined ones.
     */
    eraseKeyframes(sequence, startTime, endTime);
    for (let i = 0; i < keyframes.length; i++) {
        sequence.push({
            value: keyframes[i],
            at: motionDom.mixNumber(startTime, endTime, offset[i]),
            easing: motionUtils.getEasingForSegment(easing, i),
        });
    }
}

/**
 * Take an array of times that represent repeated keyframes. For instance
 * if we have original times of [0, 0.5, 1] then our repeated times will
 * be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
 * down to a 0-1 scale.
 *
 * `repeatDelayUnits` is the repeatDelay expressed in units of a single
 * iteration's duration, so the total span equals `(repeat + 1) + repeat * repeatDelayUnits`.
 */
function normalizeTimes(times, repeat, repeatDelayUnits = 0) {
    const totalUnits = repeat + 1 + repeat * repeatDelayUnits;
    for (let i = 0; i < times.length; i++) {
        times[i] = times[i] / totalUnits;
    }
}

function compareByTime(a, b) {
    if (a.at === b.at) {
        if (a.value === null)
            return 1;
        if (b.value === null)
            return -1;
        return 0;
    }
    else {
        return a.at - b.at;
    }
}

const defaultSegmentEasing = "easeInOut";
const MAX_REPEAT = 20;
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators, info) {
    const defaultDuration = defaultTransition.duration || 0.3;
    const animationDefinitions = new Map();
    const sequences = new Map();
    const elementCache = {};
    const timeLabels = new Map();
    let prevTime = 0;
    let currentTime = 0;
    let totalDuration = 0;
    /**
     * Build the timeline by mapping over the sequence array and converting
     * the definitions into keyframes and offsets with absolute time values.
     * These will later get converted into relative offsets in a second pass.
     *
     * Nested sequences are resolved recursively. `base` is the absolute
     * start of the sequence being resolved, so numeric `at` values inside
     * a nested sequence are relative to where it starts. Returns the time
     * the sequence ends.
     */
    const resolveSequence = (sequence, base) => {
        let sequenceEnd = base;
        const resolveTime = (at) => typeof at === "number"
            ? base + at
            : calcNextTime(currentTime, at, prevTime, timeLabels);
        for (let i = 0; i < sequence.length; i++) {
            const segment = sequence[i];
            /**
             * If this is a timeline label, mark it and skip the rest of this iteration.
             */
            if (typeof segment === "string") {
                timeLabels.set(segment, currentTime);
                continue;
            }
            else if (!Array.isArray(segment)) {
                const start = segment.at === undefined
                    ? currentTime
                    : resolveTime(segment.at);
                if ("sequence" in segment) {
                    /**
                     * Resolve the nested sequence from its start. Afterwards
                     * "<" points at its start and the next segment follows
                     * its end, as if it were a single segment.
                     */
                    prevTime = currentTime = start;
                    currentTime = resolveSequence(segment.sequence, start);
                    prevTime = start;
                    sequenceEnd = Math.max(sequenceEnd, currentTime);
                }
                else {
                    timeLabels.set(segment.name, start);
                }
                continue;
            }
            let [subject, keyframes, transition = {}] = segment;
            /**
             * If a relative or absolute time value has been specified we need to resolve
             * it in relation to the currentTime.
             */
            if (transition.at !== undefined) {
                currentTime = resolveTime(transition.at);
            }
            /**
             * Keep track of the maximum duration in this definition. This will be
             * applied to currentTime once the definition has been parsed.
             */
            let maxDuration = 0;
            const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
                const valueKeyframesAsList = keyframesAsList(valueKeyframes);
                const { delay = 0, times = motionDom.defaultOffset(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
                let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
                /**
                 * Resolve stagger() if defined.
                 */
                const calculatedDelay = typeof delay === "function"
                    ? delay(elementIndex, numSubjects)
                    : delay;
                /**
                 * If this animation should and can use a spring, generate a spring easing function.
                 */
                const numKeyframes = valueKeyframesAsList.length;
                const createGenerator = motionDom.isGenerator(type)
                    ? type
                    : generators?.[type || "keyframes"];
                if (numKeyframes <= 2 && createGenerator) {
                    /**
                     * As we're creating an easing function from a spring,
                     * ideally we want to generate it using the real distance
                     * between the two keyframes. However this isn't always
                     * possible - in these situations we use 0-100.
                     */
                    let absoluteDelta = 100;
                    if (numKeyframes === 2 &&
                        isNumberKeyframesArray(valueKeyframesAsList)) {
                        const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
                        absoluteDelta = Math.abs(delta);
                    }
                    const springTransition = {
                        ...defaultTransition,
                        ...remainingTransition,
                    };
                    if (duration !== undefined) {
                        springTransition.duration =
                            motionUtils.secondsToMilliseconds(duration);
                    }
                    const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
                    ease = springEasing.ease;
                    duration = springEasing.duration;
                }
                duration ?? (duration = defaultDuration);
                const startTime = currentTime + calculatedDelay;
                /**
                 * If there's only one time offset of 0, fill in a second with length 1
                 */
                if (times.length === 1 && times[0] === 0) {
                    times[1] = 1;
                }
                /**
                 * Fill out if offset if fewer offsets than keyframes
                 */
                const remainder = times.length - valueKeyframesAsList.length;
                remainder > 0 && motionDom.fillOffset(times, remainder);
                /**
                 * If only one value has been set, ie [1], push a null to the start of
                 * the keyframe array. This will let us mark a keyframe at this point
                 * that will later be hydrated with the previous value.
                 */
                valueKeyframesAsList.length === 1 &&
                    valueKeyframesAsList.unshift(null);
                /**
                 * Segments can't express `repeat: Infinity` or very large
                 * counts — they'd leave dead time after the segment or
                 * explode the keyframe array. Ignore with a warning.
                 */
                if (repeat) {
                    typeof process !== "undefined" &&
                        process.env.NODE_ENV !== "production" &&
                        motionUtils.warning(repeat < MAX_REPEAT, `Sequence segments can't repeat ${repeat} times — ignoring repeat option. Use a value below ${MAX_REPEAT} or apply repeat at the sequence level instead.`);
                }
                if (repeat && repeat < MAX_REPEAT) {
                    /**
                     * Express repeatDelay in units of a single iteration's duration
                     * so it can be added to the per-iteration time offsets below
                     * before they're normalized to 0-1.
                     */
                    const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
                    duration = calculateRepeatDuration(duration, repeat, repeatDelay);
                    const originalKeyframes = [...valueKeyframesAsList];
                    const originalTimes = [...times];
                    ease = Array.isArray(ease) ? [...ease] : [ease];
                    const originalEase = [...ease];
                    /**
                     * For reverse/mirror, alternate iterations play the segment
                     * backwards. mirror matches JSAnimation's mirroredGenerator:
                     * reversed keyframes, easings unchanged. reverse matches
                     * JSAnimation's iterationProgress = 1 - p: reversed
                     * keyframes, easing array reversed AND each function easing
                     * mapped through reverseEasing (string easings unchanged —
                     * they're resolved later by the keyframes engine).
                     */
                    const isFlipping = repeatType === "reverse" || repeatType === "mirror";
                    let flippedKeyframes = originalKeyframes;
                    let flippedEases = originalEase;
                    if (isFlipping) {
                        flippedKeyframes = [...originalKeyframes].reverse();
                        if (repeatType === "reverse") {
                            flippedEases = [...originalEase]
                                .reverse()
                                .map((e) => typeof e === "function"
                                ? motionUtils.reverseEasing(e)
                                : e);
                        }
                    }
                    for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
                        const isFlipped = isFlipping && repeatIndex % 2 === 0;
                        const iterKeyframes = isFlipped
                            ? flippedKeyframes
                            : originalKeyframes;
                        const iterEase = isFlipped ? flippedEases : originalEase;
                        const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
                        /**
                         * If repeatDelay is set, hold the previous iteration's
                         * final value through the delay by inserting a keyframe
                         * at the moment the next iteration begins.
                         */
                        if (repeatDelayUnits > 0) {
                            valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
                            times.push(iterStartOffset);
                            ease.push("linear");
                        }
                        valueKeyframesAsList.push(...iterKeyframes);
                        for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
                            times.push(originalTimes[keyframeIndex] + iterStartOffset);
                            ease.push(keyframeIndex === 0
                                ? "linear"
                                : motionUtils.getEasingForSegment(iterEase, keyframeIndex - 1));
                        }
                    }
                    normalizeTimes(times, repeat, repeatDelayUnits);
                }
                const targetTime = startTime + duration;
                /**
                 * Add keyframes, mapping offsets to absolute time.
                 */
                addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
                maxDuration = Math.max(calculatedDelay + duration, maxDuration);
                totalDuration = Math.max(targetTime, totalDuration);
            };
            if (motionDom.isMotionValue(subject)) {
                const subjectSequence = getSubjectSequence(subject, sequences);
                resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
            }
            else {
                const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
                const numSubjects = subjects.length;
                /**
                 * For every element in this segment, process the defined values.
                 */
                for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
                    /**
                     * Cast necessary, but we know these are of this type
                     */
                    keyframes = keyframes;
                    transition = transition;
                    const thisSubject = subjects[subjectIndex];
                    const subjectSequence = getSubjectSequence(thisSubject, sequences);
                    for (const key in keyframes) {
                        resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
                    }
                }
            }
            prevTime = currentTime;
            currentTime += maxDuration;
            sequenceEnd = Math.max(sequenceEnd, currentTime);
        }
        return sequenceEnd;
    };
    resolveSequence(sequence, 0);
    if (info) {
        info.duration = totalDuration;
        info.labels = timeLabels;
    }
    /**
     * For every element and value combination create a new animation.
     */
    sequences.forEach((valueSequences, element) => {
        for (const key in valueSequences) {
            const valueSequence = valueSequences[key];
            /**
             * Arrange all the keyframes in ascending time order.
             */
            valueSequence.sort(compareByTime);
            const keyframes = [];
            const valueOffset = [];
            const valueEasing = [];
            /**
             * For each keyframe, translate absolute times into
             * relative offsets based on the total duration of the timeline.
             */
            for (let i = 0; i < valueSequence.length; i++) {
                const { at, value, easing } = valueSequence[i];
                keyframes.push(value);
                valueOffset.push(motionUtils.progress(0, totalDuration, at));
                valueEasing.push(easing || "easeOut");
            }
            /**
             * If the first keyframe doesn't land on offset: 0
             * provide one by duplicating the initial keyframe. This ensures
             * it snaps to the first keyframe when the animation starts.
             */
            if (valueOffset[0] !== 0) {
                valueOffset.unshift(0);
                keyframes.unshift(keyframes[0]);
                valueEasing.unshift(defaultSegmentEasing);
            }
            /**
             * If the last keyframe doesn't land on offset: 1
             * provide one with a null wildcard value. This will ensure it
             * stays static until the end of the animation.
             */
            if (valueOffset[valueOffset.length - 1] !== 1) {
                valueOffset.push(1);
                keyframes.push(null);
            }
            if (!animationDefinitions.has(element)) {
                animationDefinitions.set(element, {
                    keyframes: {},
                    transition: {},
                });
            }
            const definition = animationDefinitions.get(element);
            definition.keyframes[key] = keyframes;
            /**
             * Exclude `type` from defaultTransition since springs have been
             * converted to duration-based easing functions in resolveValueSequence.
             * Including `type: "spring"` would cause JSAnimation to error when
             * the merged keyframes array has more than 2 keyframes.
             */
            const { type: _type, ...remainingDefaultTransition } = defaultTransition;
            definition.transition[key] = {
                ...remainingDefaultTransition,
                duration: totalDuration,
                ease: valueEasing,
                times: valueOffset,
                ...sequenceTransition,
            };
        }
    });
    return animationDefinitions;
}
function getSubjectSequence(subject, sequences) {
    !sequences.has(subject) && sequences.set(subject, {});
    return sequences.get(subject);
}
function getValueSequence(name, sequences) {
    if (!sequences[name])
        sequences[name] = [];
    return sequences[name];
}
function keyframesAsList(keyframes) {
    return Array.isArray(keyframes) ? keyframes : [keyframes];
}
function getValueTransition(transition, key) {
    return transition && transition[key]
        ? {
            ...transition,
            ...transition[key],
        }
        : { ...transition };
}
const isNumber = (keyframe) => typeof keyframe === "number";
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);

function isSingleValue(subject, keyframes) {
    return (motionDom.isMotionValue(subject) ||
        typeof subject === "number" ||
        (typeof subject === "string" && !isDOMKeyframes(keyframes)));
}
/**
 * Implementation
 */
function animateSubject(subject, keyframes, options, scope) {
    const animations = [];
    if (isSingleValue(subject, keyframes)) {
        animations.push(motionDom.animateSingleValue(subject, isDOMKeyframes(keyframes)
            ? keyframes.default || keyframes
            : keyframes, options ? options.default || options : options));
    }
    else {
        // Gracefully handle null/undefined subjects (e.g., from querySelector returning null)
        if (subject == null) {
            return animations;
        }
        const subjects = resolveSubjects(subject, keyframes, scope);
        const numSubjects = subjects.length;
        process.env.NODE_ENV !== "production" &&
            motionUtils.invariant(Boolean(numSubjects), "No valid elements provided.", "no-valid-elements");
        for (let i = 0; i < numSubjects; i++) {
            const thisSubject = subjects[i];
            const transition = { ...options };
            /**
             * Resolve stagger function if provided.
             */
            if ("delay" in transition &&
                typeof transition.delay === "function") {
                transition.delay = transition.delay(i, numSubjects);
            }
            if (thisSubject instanceof Element) {
                /**
                 * An element already owned by a VisualElement (a <motion.*>
                 * component or animateLayout()) animates its values so they
                 * share one renderer. Anything else is driven through
                 * styleEffect (or svgEffect) and the DOM keyframe resolver,
                 * without creating a VisualElement.
                 */
                animations.push(...motionDom.animateElement(thisSubject, keyframes, transition, motionDom.visualElementStore.get(thisSubject)));
            }
            else {
                /**
                 * Registered effects (animate.addEffect) claim non-DOM
                 * subjects before we fall back to writing plain properties.
                 */
                animations.push(...motionDom.animateEffectSubject(motionDom.findEffect(thisSubject) ?? motionDom.propEffect, thisSubject, keyframes, transition));
            }
        }
    }
    return animations;
}

function animateSequence(sequence, options, scope, info) {
    const animations = [];
    /**
     * Pre-process: replace function segments with MotionValue segments,
     * subscribe callbacks immediately
     */
    const processedSequence = sequence.map((segment) => {
        if (Array.isArray(segment) && typeof segment[0] === "function") {
            const callback = segment[0];
            const mv = motionDom.motionValue(0);
            mv.on("change", callback);
            if (segment.length === 1) {
                return [mv, [0, 1]];
            }
            else if (segment.length === 2) {
                return [mv, [0, 1], segment[1]];
            }
            else {
                return [mv, segment[1], segment[2]];
            }
        }
        return segment;
    });
    const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring: motionDom.spring }, info);
    animationDefinitions.forEach(({ keyframes, transition }, subject) => {
        animations.push(...animateSubject(subject, keyframes, transition));
    });
    return animations;
}

let markerId = 0;
/**
 * Copy a segment with a new `at`, adding a transition if it has none.
 */
function withAt(segment, at) {
    const copy = [...segment];
    const i = typeof copy[0] === "function" ? Math.max(1, copy.length - 1) : 2;
    copy[i] = { ...copy[i], at };
    return copy;
}
const label = (name, at) => at === undefined ? name : { name, at };
/**
 * Rewind to the start and stop, so every value is back at the value it
 * started from and the next build reads those values as its origin.
 */
function rewind(animation) {
    animation.pause();
    animation.time = 0;
    animation.stop();
}
/**
 * A sequence that can be changed after it's created. Segments, labels,
 * callbacks, pauses and other timelines can be added, moved and removed
 * at any point, including while it plays.
 *
 * Changes are batched and applied once per microtask. Each change
 * rebuilds the underlying animations from the timeline's starting
 * values, then restores the playhead, speed and play state, so changes
 * made while playing take effect without a jump.
 */
class Timeline {
    /**
     * `animations` are already running animations of `items`, as
     * animate() makes them. The timeline plays these until it's first
     * changed, so it costs nothing over the animations themselves until
     * then.
     */
    constructor(items = [], options = {}, animations) {
        this.options = options;
        this.entries = [];
        /**
         * The repeat the playhead is in, to detect when it starts another.
         */
        this.repeatIndex = 0;
        this.prevTime = 0;
        this.markers = [];
        this.labels = new Map();
        this.totalDuration = 0;
        this.isDirty = false;
        this.isDone = false;
        this.isRebuilding = false;
        this.playbackSpeed = 1;
        /**
         * True while this plays the animations it was created with, before
         * any change compiles it as a sequence.
         */
        this.isWrapped = false;
        this.stop = () => {
            this.flush();
            this.group?.stop();
        };
        this.isPaused = options.autoplay === false;
        this.newFinished();
        items.forEach((item) => this.add(item));
        if (animations) {
            this.isDirty = false;
            this.isWrapped = true;
            this.group = new motionDom.GroupAnimationWithThen(animations);
            this.watch();
        }
        else {
            /**
             * Build now, like animate(), so selectors resolve while a
             * scope is attached. Later changes are batched.
             */
            this.invalidate();
            this.flush();
        }
        /**
         * Register with the scope (for instance from useAnimate()) so it
         * stops when the scope is torn down.
         */
        const { scope } = options;
        if (scope) {
            scope.animations.push(this);
            this.finished.then(() => motionUtils.removeItem(scope.animations, this));
        }
    }
    /**
     * Add a segment, label or timeline. `at` overrides any `at` already
     * on the segment. The item itself is the handle for `move()` and
     * `remove()`.
     */
    add(item, at) {
        if (item instanceof Timeline)
            item.adopt(this);
        return this.push({ item, at });
    }
    /**
     * Fire a callback when the playhead crosses `at`, in either direction.
     * It receives the direction: 1 forwards, -1 backwards.
     */
    call(callback, at) {
        return this.push({
            item: callback,
            at,
            marker: { name: "_m" + markerId++, time: 0, callback },
        });
    }
    /**
     * Pause when the playhead reaches `at`.
     */
    addPause(at, callback) {
        return this.push({
            item: callback || {},
            at,
            marker: { name: "_m" + markerId++, time: 0, callback, pause: true },
        });
    }
    /**
     * Change where an item starts.
     */
    move(item, at) {
        this.entries.forEach((entry) => {
            if (entry.item === item)
                entry.at = at;
        });
        return this.invalidate();
    }
    /**
     * Remove a segment, label, callback or timeline.
     */
    remove(item) {
        if (item instanceof Timeline)
            item.parent = undefined;
        this.entries = this.entries.filter((entry) => entry.item !== item);
        return this.invalidate();
    }
    clear() {
        this.entries = [];
        return this.invalidate();
    }
    /**
     * The time of a label in seconds, or `undefined` if there's no label
     * with this name.
     */
    getLabelTime(name) {
        this.flush();
        return this.labels.get(name);
    }
    /**
     * Jump to a time in seconds or a label.
     */
    seek(position) {
        this.time = this.resolve(position);
        return this;
    }
    /**
     * Play forwards or backwards to a time or label, then pause there.
     */
    playTo(position) {
        const target = this.resolve(position);
        const speed = Math.abs(this.speed);
        this.speed = target < this.time ? -speed : speed;
        this.stopAt = target;
        this.clock || this.invalidate();
        this.play();
        return this;
    }
    reverse() {
        this.speed = -this.speed;
        this.play();
        return this;
    }
    get progress() {
        return this.duration ? this.time / this.duration : 0;
    }
    set progress(progress) {
        this.time = progress * this.duration;
    }
    get time() {
        return this.active()?.time ?? 0;
    }
    set time(time) {
        this.flush();
        this.prevTime = time;
        this.group && (this.group.time = time);
        this.repeatIndex = this.currentRepeat();
    }
    get speed() {
        return this.playbackSpeed;
    }
    set speed(speed) {
        this.playbackSpeed = speed;
        this.group && (this.group.speed = speed);
    }
    get duration() {
        this.flush();
        return this.isWrapped ? this.group.duration : this.totalDuration;
    }
    get iterationDuration() {
        this.flush();
        return this.isWrapped
            ? this.group.iterationDuration
            : this.totalDuration;
    }
    get state() {
        return this.active()?.state ?? "idle";
    }
    get startTime() {
        return this.active()?.startTime ?? null;
    }
    play() {
        this.flush();
        this.isPaused = false;
        this.group?.play();
        /**
         * Watch after playing, so it waits for the restarted animations
         * rather than the ones that already finished.
         */
        if (this.isDone) {
            this.newFinished();
            this.watch();
        }
    }
    pause() {
        this.flush();
        this.isPaused = true;
        this.group?.pause();
    }
    cancel() {
        this.flush();
        this.group?.cancel();
    }
    complete() {
        this.flush();
        this.group?.complete();
    }
    attachTimeline(timeline) {
        this.flush();
        this.scrollTimeline = timeline;
        this.detachScroll = this.group?.attachTimeline(timeline);
        return () => {
            this.scrollTimeline = undefined;
            this.detachScroll?.();
        };
    }
    then(onResolve, onReject) {
        return this.finished.then(onResolve, onReject);
    }
    /**
     * The current animations, if any are running. Skipped animations
     * (skipAnimations, reduced motion) can leave the group empty.
     */
    active() {
        this.flush();
        return this.group?.animations.length ? this.group : undefined;
    }
    push(entry) {
        this.entries.push(entry);
        return this.invalidate();
    }
    /**
     * Schedule a rebuild. A nested timeline passes changes up, as it's
     * built as part of its parent.
     */
    invalidate() {
        if (this.parent) {
            this.parent.invalidate();
        }
        else if (!this.isDirty) {
            this.isDirty = true;
            queueMicrotask(() => this.flush());
        }
        return this;
    }
    /**
     * Apply pending changes now, so reads straight after a change
     * reflect it.
     */
    flush() {
        this.isDirty && !this.parent && this.build();
    }
    adopt(parent) {
        if (this.group) {
            rewind(this.group);
            this.group = undefined;
        }
        this.isDirty = false;
        this.parent = parent;
    }
    build() {
        this.isDirty = false;
        this.isWrapped = false;
        const prev = this.group;
        let time = 0;
        if (prev) {
            this.isRebuilding = true;
            if (this.scrollTimeline) {
                /**
                 * Scroll-linked animations can't be seeked in seconds.
                 * Cancelling also returns them to their starting values,
                 * and the new build is attached to scroll instead.
                 */
                this.detachScroll?.();
                prev.cancel();
            }
            else {
                time = prev.animations.length ? prev.time : 0;
                rewind(prev);
            }
            this.isRebuilding = false;
        }
        const { defaultTransition, onComplete, onUpdate, onRepeat, onReverseComplete, autoplay, scope, ...options } = this.options;
        /**
         * Inherit MotionConfig settings from a useAnimate() scope.
         */
        const playback = {};
        if (scope?.reduceMotion !== undefined) {
            playback.reduceMotion = scope.reduceMotion;
        }
        if (scope?.skipAnimations !== undefined) {
            playback.skipAnimations = scope.skipAnimations;
        }
        Object.assign(playback, options);
        const markers = new Map();
        const info = {};
        const animations = animateSequence(this.toSequence(markers), { defaultTransition, ...playback }, scope, info);
        const labels = info.labels;
        let duration = playback.duration ?? info.duration;
        /**
         * A `duration` option stretches every animation to fit, so scale
         * label and marker times to match.
         */
        const scale = info.duration ? duration / info.duration : 1;
        labels.forEach((time, name) => labels.set(name, time * scale));
        /**
         * Markers are resolved as labels. Move them out of the label map
         * and extend the duration to reach any marker past the end.
         */
        this.markers = [];
        markers.forEach((marker, name) => {
            marker.time = labels.get(name);
            labels.delete(name);
            duration = Math.max(duration, marker.time);
            this.markers.push(marker);
        });
        this.markers.sort((a, b) => a.time - b.time);
        this.labels = labels;
        this.totalDuration = duration;
        /**
         * Callbacks, pauses and playTo() watch a linear clock. Without
         * them, the timeline is only its animations.
         */
        this.clock = this.clockAnimation = undefined;
        if (this.markers.length ||
            this.stopAt !== undefined ||
            onUpdate ||
            onRepeat) {
            const clock = (this.clock = motionDom.motionValue(0));
            clock.on("change", (t) => clock === this.clock && this.onClock(t));
            animations.unshift((this.clockAnimation = motionDom.animateSingleValue(clock, [0, duration], {
                ...playback,
                duration,
                ease: "linear",
            })));
        }
        const group = (this.group = new motionDom.GroupAnimationWithThen(animations));
        time = Math.min(time, duration);
        if (this.isDone && time < duration)
            this.newFinished();
        if (this.scrollTimeline) {
            this.detachScroll = group.attachTimeline(this.scrollTimeline);
        }
        else {
            group.speed = this.playbackSpeed;
            if (this.isPaused)
                group.pause();
            if (time)
                group.time = time;
        }
        this.repeatIndex = this.currentRepeat();
        /**
         * Treat a start from 0 as coming from just before it, so markers
         * at 0 fire.
         */
        this.prevTime = time || -1;
        this.watch();
    }
    toSequence(markers) {
        return this.entries.map(({ item, at, marker }) => {
            if (marker) {
                markers.set(marker.name, marker);
                return label(marker.name, at);
            }
            if (item instanceof Timeline) {
                return { sequence: item.toSequence(markers), at };
            }
            if (typeof item === "string")
                return label(item, at);
            if (at === undefined)
                return item;
            return Array.isArray(item)
                ? withAt(item, at)
                : { ...item, at };
        });
    }
    resolve(position) {
        this.flush();
        return typeof position === "number"
            ? position
            : calcLabelTime(position, this.labels) ?? this.time;
    }
    /**
     * The repeat the clock is in, from its time across all repeats.
     */
    currentRepeat() {
        const { repeat = 0, repeatDelay = 0, delay = 0 } = this.options;
        const cycle = this.totalDuration + repeatDelay;
        const time = (this.clockAnimation?.time ?? 0) - delay;
        return cycle && time > 0
            ? Math.min(repeat, Math.floor(time / cycle))
            : 0;
    }
    onClock(time) {
        let prevTime = this.prevTime;
        this.prevTime = time;
        if (this.isRebuilding)
            return;
        const { onUpdate, onRepeat, repeatType = "loop" } = this.options;
        onUpdate?.(time);
        const repeatIndex = this.currentRepeat();
        if (repeatIndex !== this.repeatIndex) {
            this.repeatIndex = repeatIndex;
            onRepeat?.();
            /**
             * A loop jumps the playhead back to the start, or to the
             * end when playing backwards. Finish crossing the repeat
             * that ended, then cross the new one from its edge.
             */
            if (repeatType === "loop") {
                const isForward = this.speed > 0;
                if (this.cross(prevTime, isForward ? this.totalDuration : 0)) {
                    return;
                }
                prevTime = isForward ? -1 : Infinity;
            }
        }
        this.cross(prevTime, time);
    }
    /**
     * Fire markers between two playhead times. Returns `true` if it
     * paused.
     */
    cross(from, to) {
        if (from === to)
            return;
        const isForward = to > from;
        const crossed = (t) => isForward ? t > from && t <= to : t < from && t >= to;
        const markers = isForward ? this.markers : [...this.markers].reverse();
        for (const marker of markers) {
            if (!crossed(marker.time))
                continue;
            marker.callback?.(isForward ? 1 : -1);
            if (marker.pause)
                return this.pauseAt(marker.time);
        }
        if (this.stopAt !== undefined && crossed(this.stopAt)) {
            this.pauseAt(this.stopAt);
            this.stopAt = undefined;
            return true;
        }
    }
    pauseAt(time) {
        this.pause();
        this.time = time;
        return true;
    }
    watch() {
        const group = this.group;
        group.finished.then(() => {
            if (group !== this.group || this.isDone)
                return;
            this.isDone = true;
            const { onComplete, onReverseComplete } = this.options;
            /**
             * Without onReverseComplete, onComplete fires in either
             * direction, as it does for a single animation.
             */
            const callback = (this.speed < 0 && onReverseComplete) || onComplete;
            callback?.();
            this.resolveFinished();
        });
    }
    newFinished() {
        this.isDone = false;
        this.finished = new Promise((resolve) => {
            this.resolveFinished = resolve;
        });
    }
}

function isSequence(value) {
    return Array.isArray(value) && value.some(Array.isArray);
}
/**
 * Creates an animation function that is optionally scoped
 * to a specific element.
 */
function createScopedAnimate(options = {}) {
    const { scope, reduceMotion, skipAnimations } = options;
    /**
     * Implementation
     */
    function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
        const motionScope = motionDom.activeScope.current;
        const scopeTransition = motionScope?.transition;
        const inherited = {};
        if (reduceMotion !== undefined)
            inherited.reduceMotion = reduceMotion;
        if (skipAnimations !== undefined)
            inherited.skipAnimations = skipAnimations;
        let timeline;
        if (isSequence(subjectOrSequence)) {
            const sequenceOptions = {
                ...inherited,
                ...optionsOrKeyframes,
                scope,
            };
            if (scopeTransition) {
                sequenceOptions.defaultTransition = {
                    ...scopeTransition,
                    ...sequenceOptions.defaultTransition,
                };
            }
            timeline = new Timeline(subjectOrSequence, sequenceOptions);
        }
        else {
            // Extract top-level onComplete so it doesn't get applied per-value
            const { onComplete, ...rest } = options || {};
            const transition = {
                ...inherited,
                ...scopeTransition,
                ...rest,
            };
            /**
             * Play the animations directly, and only compile them as a
             * sequence if the timeline is changed. Values animated from a
             * number are animated via their onUpdate callback instead.
             */
            const { onUpdate } = rest;
            const segment = (typeof subjectOrSequence === "number" ||
                (typeof subjectOrSequence === "string" &&
                    (typeof optionsOrKeyframes === "string" ||
                        Array.isArray(optionsOrKeyframes)))
                ? [
                    onUpdate || (() => { }),
                    Array.isArray(optionsOrKeyframes)
                        ? optionsOrKeyframes
                        : [subjectOrSequence, optionsOrKeyframes],
                    { ...transition, onUpdate: undefined },
                ]
                : [subjectOrSequence, optionsOrKeyframes, transition]);
            timeline = new Timeline([segment], { ...inherited, onComplete, scope }, animateSubject(subjectOrSequence, optionsOrKeyframes, transition, scope));
        }
        if (motionScope) {
            timeline.finished.then(motionScope.add(() => timeline.stop()));
        }
        return timeline;
    }
    return scopedAnimate;
}
const animate = /* @__PURE__ */ Object.assign(
/* @__PURE__ */ createScopedAnimate(), {
    /**
     * Register an effect so `animate()` can animate the subjects it
     * claims, for instance `animate.addEffect(threeEffect)`. The most
     * recently added effect is tested first. DOM elements are always
     * animated directly.
     */
    addEffect: motionDom.addEffect,
    removeEffect: motionDom.removeEffect,
});

function animateElements(elementOrSelector, keyframes, options, scope) {
    // Gracefully handle null/undefined elements (e.g., from querySelector returning null)
    if (elementOrSelector == null) {
        return [];
    }
    const elements = motionDom.resolveElements(elementOrSelector, scope);
    const numElements = elements.length;
    typeof process !== "undefined" &&
        process.env.NODE_ENV !== "production" &&
        motionUtils.invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
    /**
     * WAAPI doesn't support interrupting animations.
     *
     * Therefore, starting animations requires a three-step process:
     * 1. Stop existing animations (write styles to DOM)
     * 2. Resolve keyframes (read styles from DOM)
     * 3. Create new animations (write styles to DOM)
     *
     * The hybrid `animate()` function uses AsyncAnimation to resolve
     * keyframes before creating new animations, which removes style
     * thrashing. Here, we have much stricter filesize constraints.
     * Therefore we do this in a synchronous way that ensures that
     * at least within `animate()` calls there is no style thrashing.
     *
     * In the motion-native-animate-mini-interrupt benchmark this
     * was 80% faster than a single loop.
     */
    const animationDefinitions = [];
    /**
     * Step 1: Build options and stop existing animations (write)
     */
    for (let i = 0; i < numElements; i++) {
        const element = elements[i];
        const elementTransition = { ...options };
        /**
         * Resolve stagger function if provided.
         */
        if (typeof elementTransition.delay === "function") {
            elementTransition.delay = elementTransition.delay(i, numElements);
        }
        for (const valueName in keyframes) {
            let valueKeyframes = keyframes[valueName];
            if (!Array.isArray(valueKeyframes)) {
                valueKeyframes = [valueKeyframes];
            }
            const valueOptions = {
                ...motionDom.getValueTransition(elementTransition, valueName),
            };
            valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
            valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
            /**
             * If there's an existing animation playing on this element then stop it
             * before creating a new one.
             */
            const map = motionDom.getAnimationMap(element);
            const key = motionDom.animationMapKey(valueName, valueOptions.pseudoElement || "");
            const currentAnimation = map.get(key);
            currentAnimation && currentAnimation.stop();
            animationDefinitions.push({
                map,
                key,
                unresolvedKeyframes: valueKeyframes,
                options: {
                    ...valueOptions,
                    element,
                    name: valueName,
                    allowFlatten: !elementTransition.type && !elementTransition.ease,
                },
            });
        }
    }
    /**
     * Step 2: Resolve keyframes (read)
     */
    for (let i = 0; i < animationDefinitions.length; i++) {
        const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
        const { element, name, pseudoElement } = animationOptions;
        if (!pseudoElement && unresolvedKeyframes[0] === null) {
            unresolvedKeyframes[0] = motionDom.getComputedStyle(element, name);
        }
        motionDom.fillWildcards(unresolvedKeyframes);
        motionDom.applyPxDefaults(unresolvedKeyframes, name);
        /**
         * If we only have one keyframe, explicitly read the initial keyframe
         * from the computed style. This is to ensure consistency with WAAPI behaviour
         * for restarting animations, for instance .play() after finish, when it
         * has one vs two keyframes.
         */
        if (!pseudoElement && unresolvedKeyframes.length < 2) {
            unresolvedKeyframes.unshift(motionDom.getComputedStyle(element, name));
        }
        animationOptions.keyframes = unresolvedKeyframes;
    }
    /**
     * Step 3: Create new animations (write)
     */
    const animations = [];
    for (let i = 0; i < animationDefinitions.length; i++) {
        const { map, key, options: animationOptions } = animationDefinitions[i];
        const animation = new motionDom.NativeAnimation(animationOptions);
        map.set(key, animation);
        animation.finished.finally(() => map.delete(key));
        animations.push(animation);
    }
    return animations;
}

const createScopedWaapiAnimate = (scope) => {
    function scopedAnimate(elementOrSelector, keyframes, options) {
        return new motionDom.GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
    }
    return scopedAnimate;
};
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();

/**
 * A time in milliseconds, beyond which we consider the scroll velocity to be 0.
 */
const maxElapsed = 50;
const createAxisInfo = () => ({
    current: 0,
    offset: [],
    progress: 0,
    scrollLength: 0,
    targetOffset: 0,
    targetLength: 0,
    containerLength: 0,
    velocity: 0,
});
const createScrollInfo = () => ({
    time: 0,
    x: createAxisInfo(),
    y: createAxisInfo(),
});
/**
 * Also iterated with for...in as the list of axes.
 */
const axisKeys = {
    x: {
        length: "Width",
        position: "Left",
    },
    y: {
        length: "Height",
        position: "Top",
    },
};
const styles = new WeakMap();
/**
 * Whether an axis scrolls from its end, which browsers report as a scroll
 * position running from 0 to -scrollLength (#3340). This is read from style
 * rather than the sign of the scroll position, as elastic overscroll pushes
 * the position past 0 in either kind of axis (#3791).
 */
function isReversed(element, axisName) {
    /**
     * The viewport takes its writing mode and direction from <body>, which
     * not every browser reflects in the root element's computed style. It
     * doesn't adopt the root element's flex layout.
     */
    const isRoot = element === document.scrollingElement;
    const source = (isRoot && document.body) || element;
    let style = styles.get(source);
    if (!style)
        styles.set(source, (style = getComputedStyle(source)));
    const { writingMode, flexDirection } = style;
    const isInline = (axisName === "x") !== /^[vs]/.test(writingMode);
    return ((isInline
        ? (style.direction === "rtl") !== (writingMode === "sideways-lr")
        : writingMode.endsWith("rl")) !==
        /**
         * A reversed flex axis flips this. The main axis is the inline axis
         * unless flex-direction is a column.
         */
        (!isRoot &&
            style.display.includes("flex") &&
            (isInline !== (flexDirection[0] === "c")
                ? flexDirection
                : style.flexWrap).endsWith("reverse")));
}
function updateAxisInfo(element, axisName, info, time) {
    const axis = info[axisName];
    const { length, position } = axisKeys[axisName];
    const prev = axis.current;
    const prevTime = info.time;
    const scrollPosition = element[`scroll${position}`];
    axis.current = isReversed(element, axisName)
        ? -scrollPosition || 0 // Avoid -0
        : scrollPosition;
    axis.containerLength = element[`client${length}`];
    axis.targetLength = element[`scroll${length}`];
    axis.scrollLength = axis.targetLength - axis.containerLength;
    axis.offset.length = 0;
    axis.offset[0] = 0;
    axis.offset[1] = axis.scrollLength;
    axis.progress = motionUtils.clamp(0, 1, motionUtils.progress(0, axis.scrollLength, axis.current));
    const elapsed = time - prevTime;
    axis.velocity =
        elapsed > maxElapsed
            ? 0
            : motionUtils.velocityPerSecond(axis.current - prev, elapsed);
}
/**
 * Measures a scroll container. Runs once per container per frame; every
 * handler on that container derives its info from the result.
 */
function updateScrollInfo(element, info, time) {
    for (const axis in axisKeys) {
        updateAxisInfo(element, axis, info, time);
    }
    info.time = time;
}

const namedEdges = {
    start: 0,
    center: 0.5,
    end: 1,
};
function resolveEdge(edge, length, inset = 0) {
    let delta = 0;
    /**
     * If we have this edge defined as a preset, replace the definition
     * with the numerical value.
     */
    if (edge in namedEdges) {
        edge = namedEdges[edge];
    }
    /**
     * Handle unit values
     */
    if (typeof edge === "string") {
        const asNumber = parseFloat(edge);
        if (edge.endsWith("px")) {
            delta = asNumber;
        }
        else if (edge.endsWith("%")) {
            edge = asNumber / 100;
        }
        else if (edge.endsWith("vw")) {
            delta = (asNumber / 100) * document.documentElement.clientWidth;
        }
        else if (edge.endsWith("vh")) {
            delta = (asNumber / 100) * document.documentElement.clientHeight;
        }
        else {
            edge = asNumber;
        }
    }
    /**
     * If the edge is defined as a number, handle as a progress value.
     */
    if (typeof edge === "number") {
        delta = length * edge;
    }
    return inset + delta;
}

const defaultOffset = [0, 0];
function resolveOffset(offset, containerLength, targetLength, targetInset) {
    let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset;
    let targetPoint = 0;
    let containerPoint = 0;
    if (typeof offset === "number") {
        /**
         * If we're provided offset: [0, 0.5, 1] then each number x should become
         * [x, x], so we default to the behaviour of mapping 0 => 0 of both target
         * and container etc.
         */
        offsetDefinition = [offset, offset];
    }
    else if (typeof offset === "string") {
        offset = offset.trim();
        if (offset.includes(" ")) {
            offsetDefinition = offset.split(" ");
        }
        else {
            /**
             * If we're provided a definition like "100px" then we want to apply
             * that only to the top of the target point, leaving the container at 0.
             * Whereas a named offset like "end" should be applied to both.
             */
            offsetDefinition = [offset, namedEdges[offset] ? offset : `0`];
        }
    }
    targetPoint = resolveEdge(offsetDefinition[0], targetLength, targetInset);
    containerPoint = resolveEdge(offsetDefinition[1], containerLength);
    return targetPoint - containerPoint;
}

const ScrollOffset = {
    All: [
        [0, 0],
        [1, 1],
    ],
};

/**
 * Resolved offsets map to evenly spaced progress values, so progress is
 * derived from the segment index rather than building an interpolator.
 */
function offsetsToProgress(offsets, v) {
    const n = offsets.length - 1;
    if (n < 1)
        return 0;
    const reverse = offsets[0] > offsets[n];
    const at = (i) => offsets[reverse ? n - i : i];
    /**
     * Matches interpolate(), which checks for a zero-length first range
     * before reversing descending offsets.
     */
    if (offsets[0] === offsets[1] && v < at(0))
        return reverse ? 1 : 0;
    let i = 0;
    while (i < n - 1 && v >= at(i + 1))
        i++;
    const p = (i + motionUtils.progress(at(i), at(i + 1), v)) / n;
    return reverse ? 1 - p : p;
}
/**
 * Resolves offsets on both axes, as useScroll reports the target's progress
 * on each (#2546). `axis` only picks which progress scroll() reports.
 */
function resolveOffsets(info, { offset: offsetDefinition = ScrollOffset.All }) {
    for (const axis in axisKeys) {
        const axisInfo = info[axis];
        /**
         * Reset the length of the resolved offset array rather than creating a new one.
         */
        axisInfo.offset.length = 0;
        /**
         * Populate the offset array by resolving the user's offset definition into
         * a list of pixel scroll offsets.
         */
        for (let i = 0; i < offsetDefinition.length; i++) {
            axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
        }
        axisInfo.progress = motionUtils.clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
    }
}

function calcInset(element, container) {
    const inset = { x: 0, y: 0 };
    let current = element;
    while (current && current !== container) {
        if (motionDom.isHTMLElement(current)) {
            inset.x += current.offsetLeft;
            inset.y += current.offsetTop;
            current = current.offsetParent;
        }
        else if (current.tagName === "svg") {
            /**
             * This isn't an ideal approach to measuring the offset of <svg /> tags.
             * It would be preferable, given they behave like HTMLElements in most ways
             * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
             * can't use .getBBox() like most SVG elements as these provide the offset
             * relative to the SVG itself, which for <svg /> is usually 0x0.
             */
            const svgBoundingBox = current.getBoundingClientRect();
            current = current.parentElement;
            const parentBoundingBox = current.getBoundingClientRect();
            inset.x += svgBoundingBox.left - parentBoundingBox.left;
            inset.y += svgBoundingBox.top - parentBoundingBox.top;
        }
        else if (current instanceof SVGGraphicsElement) {
            const { x, y } = current.getBBox();
            inset.x += x;
            inset.y += y;
            let svg = null;
            let parent = current.parentNode;
            while (!svg) {
                if (parent.tagName === "svg") {
                    svg = parent;
                }
                parent = current.parentNode;
            }
            current = svg;
        }
        else {
            break;
        }
    }
    return inset;
}

function getTargetSize(target) {
    return "getBBox" in target && target.tagName !== "svg"
        ? target.getBBox()
        : { width: target.clientWidth, height: target.clientHeight };
}
function createOnScrollHandler(container, onScroll, info, options = {}) {
    const { target } = options;
    /**
     * In development mode ensure scroll containers aren't position: static as this makes
     * it difficult to measure their relative positions. The document scrolling element
     * is exempt: offsetParent measurements naturally resolve relative to the document.
     */
    if (process.env.NODE_ENV !== "production") {
        if (target &&
            target !== container &&
            container !== document.documentElement &&
            container !== document.scrollingElement &&
            container !== document.body) {
            motionUtils.warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
        }
    }
    /**
     * Handlers without a target or offset are notified with the container's
     * shared info object, so they measure nothing themselves.
     */
    const needsOwnInfo = target || options.offset;
    return {
        measure: (containerInfo) => {
            if (!needsOwnInfo)
                return;
            info.time = containerInfo.time;
            for (const key in axisKeys) {
                const axis = key;
                const { offset } = info[axis];
                Object.assign(info[axis], containerInfo[axis]);
                info[axis].offset = offset;
            }
            if (target && target !== container) {
                const inset = calcInset(target, container);
                const size = getTargetSize(target);
                info.x.targetOffset = inset.x;
                info.y.targetOffset = inset.y;
                info.x.targetLength = size.width;
                info.y.targetLength = size.height;
            }
            resolveOffsets(info, options);
        },
        notify: (containerInfo) => onScroll(needsOwnInfo ? info : containerInfo),
    };
}

const scrollListeners = new WeakMap();
const resizeListeners = new WeakMap();
const onScrollHandlers = new WeakMap();
const dimensionCheckProcesses = new WeakMap();
const getEventTarget = (element) => element === document.scrollingElement ? window : element;
function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
    if (!container)
        return motionUtils.noop;
    let containerHandlers = onScrollHandlers.get(container);
    /**
     * Get the onScroll handlers for this container.
     * If one isn't found, create a new one.
     */
    if (!containerHandlers) {
        containerHandlers = new Set();
        onScrollHandlers.set(container, containerHandlers);
    }
    /**
     * Create a new onScroll handler for the provided callback.
     */
    const info = createScrollInfo();
    const containerHandler = createOnScrollHandler(container, onScroll, info, options);
    containerHandlers.add(containerHandler);
    /**
     * Check if there's a scroll event listener for this container.
     * If not, create one.
     */
    if (!scrollListeners.has(container)) {
        const containerInfo = createScrollInfo();
        const measureAll = () => {
            updateScrollInfo(container, containerInfo, motionDom.frameData.timestamp);
            for (const handler of containerHandlers) {
                handler.measure(containerInfo);
            }
            motionDom.frame.preUpdate(notifyAll);
        };
        const notifyAll = () => {
            for (const handler of containerHandlers) {
                handler.notify(containerInfo);
            }
        };
        const listener = () => motionDom.frame.read(measureAll);
        scrollListeners.set(container, listener);
        const target = getEventTarget(container);
        window.addEventListener("resize", listener);
        if (container !== document.documentElement) {
            resizeListeners.set(container, motionDom.resize(container, listener));
        }
        target.addEventListener("scroll", listener);
        listener();
    }
    /**
     * Enable content size tracking if requested and not already enabled.
     */
    if (trackContentSize && !dimensionCheckProcesses.has(container)) {
        const listener = scrollListeners.get(container);
        // Store initial scroll dimensions (object is reused to avoid allocation)
        const size = {
            width: container.scrollWidth,
            height: container.scrollHeight,
        };
        // Add frame-based scroll dimension checking to detect content changes
        const checkScrollDimensions = () => {
            const newWidth = container.scrollWidth;
            const newHeight = container.scrollHeight;
            if (size.width !== newWidth || size.height !== newHeight) {
                listener();
                size.width = newWidth;
                size.height = newHeight;
            }
        };
        // Schedule with keepAlive=true to run every frame
        const dimensionCheckProcess = motionDom.frame.read(checkScrollDimensions, true);
        dimensionCheckProcesses.set(container, dimensionCheckProcess);
    }
    const listener = scrollListeners.get(container);
    motionDom.frame.read(listener, false, true);
    return () => {
        motionDom.cancelFrame(listener);
        /**
         * Check if we even have any handlers for this container.
         */
        const currentHandlers = onScrollHandlers.get(container);
        if (!currentHandlers)
            return;
        currentHandlers.delete(containerHandler);
        if (currentHandlers.size)
            return;
        /**
         * If no more handlers, remove the scroll listener too. The handler
         * set goes with it, as a measure still queued from this listener
         * would otherwise notify handlers added by a later scrollInfo call.
         */
        onScrollHandlers.delete(container);
        const scrollListener = scrollListeners.get(container);
        scrollListeners.delete(container);
        if (scrollListener) {
            getEventTarget(container).removeEventListener("scroll", scrollListener);
            resizeListeners.get(container)?.();
            window.removeEventListener("resize", scrollListener);
        }
        // Clean up scroll dimension checking
        const dimensionCheckProcess = dimensionCheckProcesses.get(container);
        if (dimensionCheckProcess) {
            motionDom.cancelFrame(dimensionCheckProcess);
            dimensionCheckProcesses.delete(container);
        }
    };
}

function canUseNativeTimeline(target) {
    if (typeof window === "undefined")
        return false;
    return target ? motionDom.supportsViewTimeline() : motionDom.supportsScrollTimeline();
}

const timelineCache = new Map();
/**
 * Native timelines are only attached to WAAPI animations. Offsets are
 * applied to each animation as a range, so a timeline is shared by every
 * offset.
 */
function getTimeline({ container, target, axis, }) {
    let containerCache = timelineCache.get(container);
    if (!containerCache) {
        containerCache = new Map();
        timelineCache.set(container, containerCache);
    }
    const targetKey = target ?? "self";
    let targetCache = containerCache.get(targetKey);
    if (!targetCache) {
        targetCache = {};
        containerCache.set(targetKey, targetCache);
    }
    return (targetCache[axis] || (targetCache[axis] = target
        ? new ViewTimeline({ subject: target, axis })
        : new ScrollTimeline({ source: container, axis })));
}

/**
 * Resolved offsets are linear in the target and container lengths, so
 * probing them gives [target progress, container progress, pixels].
 * vw/vh can resolve to 0px, so they're rejected up front.
 */
const toIntersection = (o) => {
    if (/v/u.test(o))
        return [];
    const px = resolveOffset(o, 0, 0, 0);
    return [resolveOffset(o, 0, 1, 0) - px, px - resolveOffset(o, 1, 0, 0), px];
};
const toRange = ([t, c, px]) => !px &&
    (c === 0 || c === 1) &&
    `${c ? "entry" : "exit"}-crossing ${t * 100}%`;
/**
 * Maps an offset to an equivalent ViewTimeline range. Returns undefined when
 * there isn't one, which signals the caller to fall back to JS-based scroll
 * tracking.
 */
function offsetToViewTimelineRange(offset = ScrollOffset.All) {
    if (offset.length !== 2)
        return;
    const [start, end] = offset.map(toIntersection);
    const points = [toRange(start), toRange(end)];
    const a = end[0] - start[0];
    const b = start[1] - end[1];
    if (points[0] && points[1] && (a || b)) {
        return {
            points: points,
            a,
            b,
            cover: !start[0] && a === 1 && b === 1,
        };
    }
}

function attachToAnimation(animation, options) {
    const { target, container, axis } = options;
    const range = target && offsetToViewTimelineRange(options.offset);
    /**
     * Use native timeline when:
     * - No target and no offset: ScrollTimeline
     * - Target with mappable offset: ViewTimeline with named range
     * - Otherwise: fall back to JS observe. A ScrollTimeline has no offset,
     *   so page offsets are applied by the JS timeline.
     */
    const native = canUseNativeTimeline(target) && (target ? !!range : !options.offset);
    const animations = new Map();
    const observed = new Set();
    let stopObserving;
    let reverse = false;
    /**
     * When an offset's second point comes first, its range is swapped and
     * the animation played backwards. The range is set after the direction,
     * as that's what realigns a running animation with its timeline. Chrome
     * skips that realignment when the range changes in the frame the
     * animation starts, leaving it offset for good, so it's replayed too.
     */
    const apply = (direction, waapi) => {
        const [start, end] = range.points;
        waapi.effect.updateTiming({
            direction: reverse
                ? direction === "normal"
                    ? "reverse"
                    : "alternate-reverse"
                : direction,
        });
        Object.assign(waapi, {
            rangeStart: reverse ? end : start,
            rangeEnd: reverse ? start : end,
        });
        waapi.play();
    };
    const stops = [
        animation.attachTimeline({
            /**
             * Read only by WAAPI animations, so values driven from JS don't
             * create a native timeline they won't use.
             */
            get timeline() {
                return native ? getTimeline(options) : undefined;
            },
            onAttach: range &&
                ((waapi) => {
                    const direction = waapi.effect.getTiming().direction;
                    animations.set(waapi, direction);
                    apply(direction, waapi);
                }),
            /**
             * Values driven from JS all track the offset with scrollInfo,
             * which measures once per frame for all of them.
             */
            observe: (valueAnimation) => {
                valueAnimation.pause();
                observed.add(valueAnimation);
                stopObserving || (stopObserving = scrollInfo((info) => {
                    observed.forEach((observedAnimation) => {
                        observedAnimation.time =
                            observedAnimation.iterationDuration *
                                info[axis].progress;
                    });
                }, options));
                return () => observed.delete(valueAnimation);
            },
        }),
    ];
    if (native && range) {
        const { a, b } = range;
        const length = axis === "y" ? "clientHeight" : "clientWidth";
        /**
         * Offsets like All only run forwards when the target is longer than
         * the container, so their direction is remeasured on resize.
         */
        const update = () => {
            if (reverse !==
                (reverse = a * target[length] + b * container[length] < 0)) {
                animations.forEach(apply);
            }
        };
        update();
        if (a * b < 0) {
            stops.push(motionDom.resize(update), motionDom.resize([target, container], update));
        }
    }
    return () => {
        stops.forEach((stop) => stop());
        stopObserving?.();
    };
}

function scroll(onScroll, { axis = "y", source, container = document.scrollingElement, ...options } = {}) {
    if (!container)
        return motionUtils.noop;
    const optionsWithDefaults = {
        axis,
        container: source || container,
        ...options,
    };
    if (typeof onScroll === "function") {
        const callback = motionDom.bindToScope(onScroll);
        return motionDom.addToScope(scrollInfo((info) => callback(info[axis].progress, info), optionsWithDefaults));
    }
    return motionDom.addToScope(attachToAnimation(onScroll, optionsWithDefaults));
}

const thresholds = {
    some: 0,
    all: 1,
};
function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) {
    const elements = motionDom.resolveElements(elementOrSelector);
    onStart = motionDom.bindToScope(onStart);
    const activeIntersections = new WeakMap();
    const onIntersectionChange = (entries) => {
        entries.forEach((entry) => {
            const onEnd = activeIntersections.get(entry.target);
            /**
             * If there's no change to the intersection, we don't need to
             * do anything here.
             */
            if (entry.isIntersecting === Boolean(onEnd))
                return;
            if (entry.isIntersecting) {
                const newOnEnd = onStart(entry.target, entry);
                if (typeof newOnEnd === "function") {
                    activeIntersections.set(entry.target, newOnEnd);
                }
                else {
                    observer.unobserve(entry.target);
                }
            }
            else if (typeof onEnd === "function") {
                onEnd(entry);
                activeIntersections.delete(entry.target);
            }
        });
    };
    const observer = new IntersectionObserver(onIntersectionChange, {
        root,
        rootMargin,
        threshold: typeof amount === "number" ? amount : thresholds[amount],
    });
    elements.forEach((element) => observer.observe(element));
    return motionDom.addToScope(() => observer.disconnect());
}

Object.defineProperty(exports, "delay", {
    enumerable: true,
    get: function () { return motionDom.delayInSeconds; }
});
exports.animate = animate;
exports.animateMini = animateMini;
exports.createScopedAnimate = createScopedAnimate;
exports.inView = inView;
exports.scroll = scroll;
exports.scrollInfo = scrollInfo;
Object.keys(motionDom).forEach(function (k) {
    if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
        enumerable: true,
        get: function () { return motionDom[k]; }
    });
});
Object.keys(motionUtils).forEach(function (k) {
    if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
        enumerable: true,
        get: function () { return motionUtils[k]; }
    });
});
//# sourceMappingURL=dom.js.map