import { warnOnce, secondsToMilliseconds } from 'motion-utils';
import { GroupAnimation } from '../animation/GroupAnimation.mjs';
import { NativeAnimation } from '../animation/NativeAnimation.mjs';
import { NativeAnimationWrapper } from '../animation/NativeAnimationWrapper.mjs';
import { getValueTransition } from '../animation/utils/get-value-transition.mjs';
import { mapEasingToNativeEasing } from '../animation/waapi/easing/map-easing.mjs';
import { applyGeneratorOptions } from '../animation/waapi/utils/apply-generator.mjs';
import { cornerRadiusProps } from '../utils/border-radius.mjs';
import { resolveElements } from '../utils/resolve-elements.mjs';
import { assignViewTransitionNames, releaseViewTransitionNames } from './utils/assign-names.mjs';
import { chooseLayerType } from './utils/choose-layer-type.mjs';
import { css } from './utils/css.mjs';
import { getViewAnimationLayerInfo } from './utils/get-layer-info.mjs';
import { getViewAnimations } from './utils/get-view-animations.mjs';
import { hasTarget } from './utils/has-target.mjs';

const definitionNames = ["layout", "enter", "exit", "new", "old"];
/**
 * Whether a computed border-radius is square (every component zero). Splitting
 * on whitespace handles two-value/elliptical radii like "0px 20px" - a leading
 * `parseFloat` alone would misread the non-zero vertical radius as square.
 */
const isSquareRadius = (radius) => radius.split(" ").every((value) => parseFloat(value) === 0);
/**
 * The `ViewTransitionTarget` buckets driving each generated layer type, in
 * priority order - the inverse of `chooseLayerType`. The new view is driven by
 * `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
 * bucket; they follow the default layout timing.
 */
const typeBuckets = {
    group: ["layout"],
    new: ["new", "enter"],
    old: ["old", "exit"],
};
/**
 * Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
 * `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
 * from 1. `enter` prefers the matching `exit` value over these (see below).
 */
const ORIGIN_DEFAULTS = {
    new: { opacity: 0, scale: 0.85 },
    old: { opacity: 1, scale: 1 },
};
/**
 * How much two box aspect ratios must differ before a morph is treated as
 * aspect-changing (and so worth cropping). Matches the projection engine's
 * `preserve-aspect` threshold, so small layout jitter doesn't trigger a crop.
 */
const ASPECT_TOLERANCE = 0.2;
function startViewAnimation(builder) {
    const { update, targets, resolveDefs, cropOverride, pairs, classNames, flatGroups, options: defaultOptions, } = builder;
    if (!document.startViewTransition) {
        // An async IIFE (not `new Promise(async …)`) so a throwing/rejecting
        // update rejects this promise rather than leaving it unsettled.
        return (async () => {
            await update();
            return new GroupAnimation([]);
        })();
    }
    /**
     * Resolve any selector/Element targets to layer names, assigning a
     * `view-transition-name` to each element as we go. We run this before the
     * update (so the elements are captured in the old snapshot) and again
     * after it (for the new snapshot). An element present in both keeps the
     * same name and animates as a single `group` layer.
     */
    const nameRegistry = new Map();
    const assigned = [];
    /**
     * Elements we tagged with a `view-transition-class` (via `.class()`),
     * tracked separately from `assigned` so cleanup removes the class without
     * ever stripping an author's own inline `view-transition-name`.
     */
    const classed = [];
    /**
     * Elements we set a `view-transition-group` on (for nesting), tracked for
     * cleanup. `clipChildren` collects the names of nested parents that clip in
     * the live layout, so their `::view-transition-group-children` is clipped
     * through the transition - not just once the live DOM takes back over.
     */
    const grouped = [];
    const clipChildren = new Set();
    const layerTargets = new Map();
    const croppedNames = new Set();
    /**
     * Each layer's explicit `.crop(true | false)` override (by resolved name),
     * so `finalizeCrop` can let an author's choice win over the morph default.
     */
    const cropForName = new Map();
    /**
     * Each layer's stagger position (index + total) within its subject, per
     * snapshot. Resolving against the snapshot the layer belongs to keeps
     * stagger correct when `update()` replaces the matched elements, and lets
     * us skip a layer that's absent from a snapshot (e.g. an exited element
     * has no `new` pseudo-element).
     */
    const layerStagger = new Map();
    /**
     * Names allocated for a paired subject in the old snapshot, replayed onto
     * its new-snapshot target so both ends share a layer and morph.
     */
    const pairNames = new Map();
    /**
     * The old (`from`) elements of each paired subject, so their names can be
     * transferred off before the new (`to`) elements inherit them.
     */
    const pairFrom = new Map();
    const resolveLayers = (phase) => {
        targets.forEach((target, definition) => {
            const className = classNames.get(definition);
            /**
             * Nest each resolved layer under its DOM-ancestor layer by default
             * (`contain`), so an ancestor's clip/transform/opacity reach it
             * through the transition; `.group(false)` opts a subject out (`none`)
             * to stay flat and escape. Skipped for root / pre-named layers, which
             * aren't elements we resolve and style.
             */
            const group = definition === "root" || !resolveDefs.has(definition)
                ? undefined
                : flatGroups.has(definition)
                    ? "none"
                    : "contain";
            let names;
            if (definition === "root" || !resolveDefs.has(definition)) {
                names = [definition];
            }
            else if (pairs.has(definition)) {
                /**
                 * Paired morph: name the old target in the old snapshot, then
                 * force the same name(s) onto the new target in the new one, so
                 * two different elements morph as a single layer.
                 */
                if (phase === "old") {
                    pairFrom.set(definition, resolveElements(definition));
                    names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
                    pairNames.set(definition, names);
                }
                else {
                    /**
                     * Transfer the name(s) off the `from` elements before the
                     * `to` elements inherit them. A `from` that survives into
                     * the new snapshot (e.g. hidden with `visibility: hidden`
                     * rather than removed) would otherwise keep the name and
                     * collide - "duplicate view-transition-name".
                     */
                    for (const el of pairFrom.get(definition) ?? []) {
                        el.style?.removeProperty("view-transition-name");
                        /**
                         * Drop the old end from the registry too, so the new
                         * end alone supplies this name's `new` crop radii - we
                         * neither re-measure nor get ordered by a stale element.
                         */
                        nameRegistry.delete(el);
                    }
                    names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed, group, grouped, clipChildren);
                }
            }
            else {
                names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
            }
            // Record any explicit `.crop(true | false)` per resolved name; the
            // crop set itself is computed later by `finalizeCrop` (it needs both
            // snapshots to know which morphs change aspect ratio).
            const override = cropOverride.get(definition);
            names.forEach((name, index) => {
                /**
                 * If two subjects resolve to the same element, merge their
                 * definitions so neither subject's animations are dropped.
                 */
                const existing = layerTargets.get(name);
                layerTargets.set(name, existing && existing !== target
                    ? { ...existing, ...target }
                    : target);
                if (override !== undefined)
                    cropForName.set(name, override);
                const stagger = layerStagger.get(name) ?? {};
                stagger[phase] = [index, names.length];
                layerStagger.set(name, stagger);
            });
        });
    };
    /**
     * The stagger index/total for a layer, resolved against the snapshot it
     * belongs to. Returns index -1 when the layer is absent from that snapshot
     * so the caller can skip a pseudo-element that doesn't exist.
     */
    const staggerPosition = (name, type) => {
        const stagger = layerStagger.get(name);
        const position = type === "old"
            ? stagger?.old
            : type === "new"
                ? stagger?.new
                : // group / group-children / image-pair persist across both.
                    stagger?.new ?? stagger?.old;
        return position ?? [-1, 1];
    };
    /**
     * Merge default + per-layer transition options for a generated layer and
     * resolve any stagger/delay function against this element's position. Used
     * by both the morph-retiming and crop corner-radius passes.
     */
    const resolveLayerTransition = (target, type, transitionName, index, total) => {
        const transition = mergeTransition(getValueTransition(defaultOptions, transitionName), getValueTransition((layerOptions(target, type) ?? {}), transitionName));
        if (typeof transition.delay === "function") {
            transition.delay = transition.delay(index, total);
        }
        return transition;
    };
    /**
     * Resolve a layer's group (`layout`) timing to plain WAAPI values: native
     * ms `delay`/`duration` and a baked `ease`. The single source of group
     * timing, shared by the generated-group retiming and the crop corner-radius
     * pass so the rounded clip animates on exactly the box's timing. It returns
     * no generator `type` (the WAAPI-only `NativeAnimation` rejects a string
     * type) nor `repeat`/`times` (which the group's `updateTiming` ignores), so
     * none of them can leak into the radius animation and desync it.
     */
    const resolveGroupTiming = (name) => {
        const [index, total] = staggerPosition(name, "group");
        const transition = resolveLayerTransition(layerTargets.get(name), "group", "layout", index === -1 ? 0 : index, total);
        transition.duration && (transition.duration = secondsToMilliseconds(transition.duration));
        const { delay = 0, duration, ease } = applyGeneratorOptions(transition);
        return { delay: secondsToMilliseconds(delay), duration, ease };
    };
    /**
     * Each layer's measured box + corner radii per snapshot. The box lets
     * `finalizeCrop` tell whether a morph's aspect ratio changed (the only case
     * worth cropping); the radii let a cropped morph's group clip animate each
     * corner from the old element's radius to the new element's, keeping it
     * rounded where `overflow: clip` would otherwise square the corners.
     *
     * We never flatten the source for capture (a snapshot is a paint of the live
     * DOM, so squaring an element just for its capture would flash one real
     * square frame). For an aspect-changing morph `object-fit: cover` crops each
     * snapshot's own baked corners off-screen mid-morph, so the animated clip is
     * the only visible corner; a near-same-aspect forced crop (`.crop(true)`)
     * can't hide the outgoing snapshot's silhouette, but the endpoints coincide.
     */
    const cropMeasurements = new Map();
    const measureLayers = (phase) => nameRegistry.forEach((name, element) => {
        const el = element;
        const rect = el.getBoundingClientRect?.();
        if (rect && rect.height) {
            const style = getComputedStyle(el);
            const radii = {};
            for (const corner of cornerRadiusProps) {
                radii[corner] = style[corner];
            }
            const entry = cropMeasurements.get(name) ?? {};
            entry[phase] = { width: rect.width, height: rect.height, radii };
            cropMeasurements.set(name, entry);
        }
    });
    /**
     * With both snapshots measured, settle which layers crop. The default crops
     * only a morph whose aspect ratio *changes* between snapshots - the one case
     * where `object-fit: cover` does real work. A same-aspect morph or a
     * fade-only layer is left uncropped: its corners scale naturally (no flash
     * from squaring, no `overflow: clip` eating its shadow) and a backdrop can't
     * be clipped to nothing. An explicit `.crop(true | false)` overrides either
     * way. Runs after both snapshots are measured, since aspect needs both.
     */
    const finalizeCrop = () => {
        croppedNames.clear();
        for (const name of layerStagger.keys()) {
            if (name === "root")
                continue;
            // An explicit `.crop(true | false)` wins; otherwise crop a morph
            // whose aspect ratio changed.
            if (cropForName.get(name) ?? aspectChanged(name)) {
                croppedNames.add(name);
            }
        }
    };
    /**
     * Whether a layer is a morph whose box aspect ratio changed between
     * snapshots (beyond a small tolerance). Fade-only layers (one snapshot) are
     * never "changed".
     */
    const aspectChanged = (name) => {
        const box = cropMeasurements.get(name);
        if (!box?.old || !box?.new || !box.old.height || !box.new.height) {
            return false;
        }
        return (Math.abs(box.old.width / box.old.height -
            box.new.width / box.new.height) > ASPECT_TOLERANCE);
    };
    /**
     * Write the persistent view-transition CSS: suppress root capture when the
     * root has no animations of its own; force linear timing (baked into the
     * keyframes, so we can retime later via updateTiming); and clip +
     * object-fit: cover every cropped morph (the UA default overflows on
     * aspect-ratio change).
     *
     * `css.commit` replaces rather than appends, so we re-set the full rule set
     * each call - the crop rules are only known after `finalizeCrop` runs in the
     * update callback, so the second call writes them.
     */
    const commitViewCSS = () => {
        if (!hasTarget("root", targets)) {
            css.set(":root", { "view-transition-name": "none" });
        }
        css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
        croppedNames.forEach((name) => {
            css.set(`::view-transition-group(${name})`, { overflow: "clip" });
            css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
        });
        /**
         * Clip the nested children of any layer that clips in the live layout,
         * so a wrapper crops its child for the whole morph (mirroring the DOM)
         * rather than only at the live-DOM handoff. No-op on browsers without
         * nested view-transition groups.
         */
        clipChildren.forEach((name) => {
            css.set(`::view-transition-group-children(${name})`, {
                overflow: "clip",
            });
        });
        css.commit(); // Write
    };
    const cleanup = () => {
        releaseViewTransitionNames(assigned, classed, grouped);
        css.remove(); // Write
    };
    const callback = async () => {
        await update();
        /**
         * Re-resolve so elements created by the update are named for the new
         * snapshot, then measure them. With both snapshots measured we can
         * settle the crop set (aspect-changing morphs + forced).
         */
        resolveLayers("new");
        measureLayers("new");
        finalizeCrop();
        /**
         * Re-commit the crop CSS unconditionally: `finalizeCrop` is computed
         * here (after both snapshots are measured), so the clip rules must be
         * (re)written to match the settled set.
         */
        commitViewCSS();
    };
    let transition;
    try {
        resolveLayers("old");
        /**
         * Measure the old snapshot against the optimistic crop set (the new
         * snapshot doesn't exist yet, so aspect change can't be known here;
         * `finalizeCrop` settles it post-update).
         */
        measureLayers("old");
        commitViewCSS();
        transition = document.startViewTransition(callback);
    }
    catch (error) {
        /**
         * The prelude writes inline names before the transition exists. If it
         * throws (e.g. startViewTransition rejects in a bad UA state), release
         * them so we neither leak DOM state nor stall the queue on a promise
         * that never settles - hand back a rejection it can advance past.
         */
        cleanup();
        return Promise.reject(error);
    }
    transition.finished.finally(cleanup);
    return new Promise((resolve, reject) => {
        transition.ready
            .then(() => {
            const generatedViewAnimations = getViewAnimations();
            const animations = [];
            /**
             * Create animations for each of our explicitly-defined subjects.
             * `opacityAnimated` additionally tracks which `${name}:${type}`
             * we faded, so we can keep the UA `plus-lighter` blend only for a
             * genuine opacity crossfade (both sides fading) and drop it for a
             * slide/transform, where additive compositing would flash bright.
             */
            const explicitlyAnimated = new Set();
            const opacityAnimated = new Set();
            layerTargets.forEach((target, name) => {
                const stagger = layerStagger.get(name);
                /**
                 * Presence: `enter` only fires for a pure newcomer (a new
                 * view with no old), `exit` only for a pure leaver. A
                 * survivor (both) gets neither - it just morphs.
                 */
                const enterApplies = !!stagger?.new && !stagger?.old;
                const exitApplies = !!stagger?.old && !stagger?.new;
                for (const key of definitionNames) {
                    if (!target[key])
                        continue;
                    if (key === "enter" && !enterApplies)
                        continue;
                    if (key === "exit" && !exitApplies)
                        continue;
                    const type = chooseLayerType(key);
                    const [index, total] = staggerPosition(name, type);
                    // Skip a layer absent from its snapshot.
                    if (index === -1)
                        continue;
                    const { keyframes, options } = target[key];
                    for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
                        // Skip only missing values - `0` (e.g. opacity: 0)
                        // is valid and must reach the from-value inference.
                        if (valueKeyframes == null)
                            continue;
                        /**
                         * The view path hands keyframes straight to WAAPI,
                         * so Motion's `x`/`y` shorthands (compiled to
                         * `transform` only via the value pipeline) have no
                         * effect. Warn and skip - use `transform`/`translate`.
                         */
                        if (valueName === "x" || valueName === "y") {
                            warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
                            continue;
                        }
                        /**
                         * enter/exit win over new/old on a shared property -
                         * skip it here when the gated bucket also defines it.
                         */
                        if (key === "new" &&
                            enterApplies &&
                            target.enter?.keyframes[valueName] != null) {
                            continue;
                        }
                        if (key === "old" &&
                            exitApplies &&
                            target.exit?.keyframes[valueName] != null) {
                            continue;
                        }
                        const valueOptions = mergeTransition(getValueTransition(defaultOptions, valueName), getValueTransition(options, valueName));
                        /**
                         * Infer an origin for a single-value keyframe. An
                         * `enter` mirrors the matching `exit` value (a
                         * defined exit reverses into the enter for free);
                         * otherwise the per-type default (opacity 0/1, scale
                         * 0.85). No default -> left as-is (animates from the
                         * live value).
                         *
                         * `new`/`old` fire for survivors too, where only the
                         * opacity crossfade default applies - a transform
                         * default like scale 0.85 would pop a persisting
                         * element, so gate it on the layer actually
                         * entering/leaving.
                         */
                        if (!Array.isArray(valueKeyframes)) {
                            const exitValue = key === "enter"
                                ? target.exit?.keyframes[valueName]
                                : undefined;
                            const allowDefault = valueName === "opacity" ||
                                (type === "new" ? enterApplies : exitApplies);
                            const from = exitValue != null
                                ? Array.isArray(exitValue)
                                    ? exitValue[exitValue.length - 1]
                                    : exitValue
                                : allowDefault
                                    ? ORIGIN_DEFAULTS[type]?.[valueName]
                                    : undefined;
                            if (from !== undefined) {
                                valueKeyframes = [from, valueKeyframes];
                            }
                        }
                        /**
                         * Resolve stagger function if provided, per element
                         * across this subject's resolved layers.
                         */
                        if (typeof valueOptions.delay === "function") {
                            valueOptions.delay = valueOptions.delay(index, total);
                        }
                        valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
                        valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
                        animations.push(new NativeAnimation({
                            ...valueOptions,
                            element: document.documentElement,
                            name: valueName,
                            pseudoElement: `::view-transition-${type}(${name})`,
                            keyframes: valueKeyframes,
                        }));
                        explicitlyAnimated.add(`${name}:${type}`);
                        if (valueName === "opacity") {
                            opacityAnimated.add(`${name}:${type}`);
                        }
                    }
                }
            });
            /**
             * Handle browser generated animations
             */
            for (const animation of generatedViewAnimations) {
                if (animation.playState === "finished")
                    continue;
                const { effect } = animation;
                if (!effect || !(effect instanceof KeyframeEffect))
                    continue;
                const { pseudoElement } = effect;
                if (!pseudoElement)
                    continue;
                const name = getViewAnimationLayerInfo(pseudoElement);
                if (!name)
                    continue;
                const targetDefinition = layerTargets.get(name.layer);
                /**
                 * We built our own animation for this layer, so drop the
                 * browser-generated fade we're replacing. The UA
                 * `plus-lighter` blend is a *separate* generated animation on
                 * the same pseudo (it sets `mix-blend-mode` in its keyframes):
                 * keep it *only* for a true opacity crossfade - both sides
                 * fading - so a symmetric crossfade composites without
                 * darkening, but a slide/transform (where both layers stay
                 * opaque and overlap) doesn't flash bright from the addition.
                 */
                if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
                    const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
                        opacityAnimated.has(`${name.layer}:old`);
                    if (isCrossfade &&
                        effect
                            .getKeyframes()
                            .some((keyframe) => keyframe.mixBlendMode)) {
                        animations.push(new NativeAnimationWrapper(animation));
                    }
                    else {
                        animation.cancel();
                    }
                    continue;
                }
                /**
                 * Drop the orphaned half of the default crossfade. The UA
                 * fades old out and new in as a *pair*; if the opposing half
                 * was explicitly overridden with something other than an
                 * opacity fade (a clip or transform reveal), this side's
                 * default opacity fade has no partner - left to run it would
                 * dissolve what should be a static backdrop (e.g.
                 * `.new({ clipPath })` should reveal over a still old view,
                 * not fade the old out around the growing clip). Cancel it -
                 * and its `plus-lighter` sibling on the same pseudo, which
                 * would otherwise flash bright where the two opaque layers
                 * overlap. A genuine crossfade (the opposing side also fading
                 * opacity) keeps both halves and is handled above.
                 */
                const opposite = name.type === "old"
                    ? "new"
                    : name.type === "new"
                        ? "old"
                        : undefined;
                if (opposite &&
                    explicitlyAnimated.has(`${name.layer}:${opposite}`) &&
                    !opacityAnimated.has(`${name.layer}:${opposite}`)) {
                    animation.cancel();
                    continue;
                }
                /**
                 * Otherwise retime the browser-generated animation to
                 * Motion's timing. This auto-enables the layout (group)
                 * morph for any resolved/named target, and applies the
                 * default timing to old/new layers we haven't explicitly
                 * overridden.
                 *
                 * group + group-children both follow the layout timing so
                 * the nesting container stays in sync with the morph.
                 */
                /**
                 * A survivor's old + new are the two halves of one
                 * `plus-lighter` crossfade. They must share identical timing
                 * (so their opacities stay mirrored and sum to 1 - else the
                 * additive blend flashes bright wherever both are partly
                 * visible) and fade linearly (the bounce belongs on the
                 * group's geometry, not the opacity). So time them as the
                 * group, rather than via their own - potentially staggered,
                 * or enter/exit-derived - old/new options.
                 */
                const stagger = layerStagger.get(name.layer);
                const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
                    !!stagger?.old &&
                    !!stagger?.new;
                let timing;
                if (name.type.startsWith("group")) {
                    // group + group-children follow the resolved group
                    // timing - the single source shared with the crop
                    // corner-radius pass below.
                    const { delay, duration, ease } = resolveGroupTiming(name.layer);
                    timing = {
                        delay,
                        duration,
                        easing: mapEasingToNativeEasing(ease, duration),
                    };
                }
                else {
                    const timingType = isMorphCrossfade ? "group" : name.type;
                    const [index, total] = staggerPosition(name.layer, timingType);
                    const transitionName = timingType === "group" ? "layout" : "";
                    let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
                    /**
                     * The crossfade should resolve at the spring's
                     * *perceptual* (visual) duration - the geometry can keep
                     * bouncing, but the opacity shouldn't drag through the
                     * settle. So capture `visualDuration` before
                     * `applyGeneratorOptions` replaces it with the full
                     * overshoot duration, and use it for the fade.
                     */
                    const visualDuration = animationTransition.visualDuration;
                    animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
                    animationTransition =
                        applyGeneratorOptions(animationTransition);
                    timing = {
                        delay: secondsToMilliseconds(animationTransition.delay ?? 0),
                        duration: isMorphCrossfade && visualDuration !== undefined
                            ? secondsToMilliseconds(visualDuration)
                            : animationTransition.duration,
                        easing: isMorphCrossfade
                            ? "linear"
                            : mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration),
                    };
                }
                effect.updateTiming(timing);
                animations.push(new NativeAnimationWrapper(animation));
            }
            /**
             * Round each cropped layer's clip. Its `::view-transition-group`
             * has `overflow: clip`, which would otherwise square the corners
             * mid-morph; animate each corner from the old element's radius to
             * the new element's so the crop stays rounded. Timed as the group
             * (`layout`) so the radius tracks the morphing box.
             */
            cropMeasurements.forEach((entry, name) => {
                if (!croppedNames.has(name))
                    return;
                // Reuse the group's resolved timing - native ms delay/
                // duration + a baked ease, with no generator `type` or
                // repeat/times to leak into (or throw inside) NativeAnimation.
                const { delay, duration, ease } = resolveGroupTiming(name);
                for (const corner of cornerRadiusProps) {
                    // `||` (not `??`) so an empty measurement falls back to
                    // the other snapshot rather than an invalid keyframe.
                    const from = entry.old?.radii[corner] ||
                        entry.new?.radii[corner] ||
                        "0px";
                    const to = entry.new?.radii[corner] ||
                        entry.old?.radii[corner] ||
                        "0px";
                    // Nothing to round if the corner is square at both ends.
                    if (isSquareRadius(from) && isSquareRadius(to))
                        continue;
                    animations.push(new NativeAnimation({
                        element: document.documentElement,
                        name: corner,
                        pseudoElement: `::view-transition-group(${name})`,
                        keyframes: [from, to],
                        delay,
                        duration,
                        ease,
                    }));
                }
            });
            resolve(new GroupAnimation(animations));
        })
            .catch(() => 
        /**
         * `ready` rejects when the transition is skipped - no visual
         * change, or superseded by an interrupting transition. The DOM
         * update still applied, so settle with no animations rather than
         * surfacing it as an error to an awaiting caller. A genuine
         * failure in `update()` rejects `updateCallbackDone` (already
         * settled by now), so propagate that instead.
         */
        transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
    });
}
/**
 * The options that should time a given generated layer type, so a retimed
 * group/old/new picks up any per-target transition the user provided. Checks
 * the type's buckets in priority order (e.g. `new` before `enter`).
 */
function layerOptions(target, type) {
    for (const bucket of typeBuckets[type] ?? []) {
        const options = target?.[bucket]?.options;
        if (options)
            return options;
    }
}
/**
 * Merge a base transition (e.g. the default `options`) with a per-layer/value
 * override. An explicit `duration` on the override must win over an inherited
 * generator's own timing: a spring prefers `visualDuration`, and
 * `spring.applyToOptions` overwrites `duration` with the computed settle time -
 * so without this the override is silently discarded. Dropping the inherited
 * `type`/`visualDuration` makes the layer a plain tween of that duration, unless
 * it asked for its own generator `type`/`visualDuration`.
 */
function mergeTransition(base, override) {
    const merged = { ...base, ...override };
    if (override.duration !== undefined) {
        if (override.visualDuration === undefined)
            delete merged.visualDuration;
        if (override.type === undefined)
            delete merged.type;
    }
    return merged;
}

export { startViewAnimation };
//# sourceMappingURL=start.mjs.map
