UNPKG

@humanspeak/svelte-motion

Version:

Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values

785 lines (784 loc) 32.2 kB
import { mergeTransitions } from './animation'; import { pwLog } from './log'; import { animate } from 'motion'; import { getContext, setContext } from 'svelte'; import { createSubscriber } from 'svelte/reactivity'; /** * Context key for `AnimatePresence`. * * Used with Svelte's context API to provide/register presence management. */ const ANIMATE_PRESENCE_CONTEXT = Symbol('animate-presence-context'); /** * Context key for tracking nesting depth within AnimatePresence. * * Used to enforce key requirements only on direct children (depth 0), * matching Framer Motion behavior where only immediate children need keys. */ const PRESENCE_DEPTH_CONTEXT = Symbol('presence-depth-context'); const isHTMLElement = (element) => element instanceof HTMLElement; const readNumericStyle = (value, fallback) => { const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : fallback; }; /** * Measure an element for `mode="popLayout"` using upstream Motion's coordinate * model: offsets are captured relative to `offsetParent`, not the viewport. * Ancestor scroll is intentionally not subtracted here because upstream * `PopChild` snapshots raw `offsetTop`/`offsetLeft`; any Svelte clone fallback * that breaks that containing-block relationship must compensate at the clone * application layer instead. * * @param element The exiting element to measure while it is still in layout. * @param computedStyle The computed style for the exiting element. * @returns A snapshot that can be reapplied to an absolutely positioned exit node. */ export const measurePopLayoutSnapshot = (element, computedStyle = getComputedStyle(element)) => { const parent = element.offsetParent; const parentWidth = isHTMLElement(parent) ? parent.offsetWidth || 0 : 0; const parentHeight = isHTMLElement(parent) ? parent.offsetHeight || 0 : 0; const rect = element.getBoundingClientRect(); const width = readNumericStyle(computedStyle.width, element.offsetWidth || rect.width); const height = readNumericStyle(computedStyle.height, element.offsetHeight || rect.height); const top = element.offsetTop; const left = element.offsetLeft; return { width, height, top, left, right: parentWidth - width - left, bottom: parentHeight - height - top, direction: computedStyle.direction || 'ltr' }; }; /** * Convert a `popLayout` snapshot to absolute-positioned clone styles. * * @param snapshot The snapshot captured before the child exited. * @param anchorX The horizontal edge to preserve. * @param anchorY The vertical edge to preserve. * @returns Inline styles equivalent to upstream `PopChild`'s injected rule. */ export const resolvePopLayoutStyles = (snapshot, anchorX = 'left', anchorY = 'top') => { const isRTL = snapshot.direction === 'rtl'; const useLeft = anchorX === 'left' ? !isRTL : isRTL; const xProperty = useLeft ? 'left' : 'right'; const xValue = useLeft ? snapshot.left : snapshot.right; const yProperty = anchorY === 'bottom' ? 'bottom' : 'top'; const yValue = anchorY === 'bottom' ? snapshot.bottom : snapshot.top; return { position: 'absolute', width: `${snapshot.width}px`, height: `${snapshot.height}px`, [xProperty]: `${xValue}px`, [yProperty]: `${yValue}px` }; }; /** * Reset any CSS transforms on the element's inline style. * * Ensures the exiting clone is not additionally offset or scaled by an * inherited transform. Applies to standard and vendor-prefixed properties. * * @param element The element whose inline transform properties should be cleared. */ const resetTransforms = (element) => { const s = element.style; s.transform = 'none'; s.webkitTransform = 'none'; s.msTransform = 'none'; s.MozTransform = 'none'; s.OTransform = 'none'; }; const findLayoutInsertionParent = (element) => { let before = element; let parent = element.parentElement; while (parent && getComputedStyle(parent).display === 'contents') { before = parent; parent = parent.parentElement; } return parent ? { parent, before } : null; }; /** * Create a new `AnimatePresence` context instance. * * - Maintains a registry of children keyed by a unique string. * - On unregister, if a child has an `exit` definition, a visual clone is * created at its last known position and animated using Motion. * * @param context Optional callbacks, e.g. `onExitComplete`. * @returns An object implementing the `AnimatePresenceContext` API. */ /** * Create a new `AnimatePresence` context instance. * * Manages child registration and on unregistration performs exit animation by * cloning the DOM node, freezing its last known rect/styles, and animating * the clone using Motion. Calls `onExitComplete` once when all exits settle. * * @param context Optional callbacks, for example `onExitComplete`. * @returns A presence context with register/update/unregister APIs. */ export const createAnimatePresenceContext = (context) => { // Default initial to true (animate on first mount) unless explicitly false const initial = context.initial !== false; // Default mode to 'sync' if not specified const mode = context.mode ?? 'sync'; let latestCustom = context.custom; const customSubscribers = new Set(); const trackCustom = createSubscriber((update) => { customSubscribers.add(update); return () => customSubscribers.delete(update); }); const getCustom = () => context.getCustom?.() ?? latestCustom; const setCustom = (custom) => { if (Object.is(latestCustom, custom)) return; latestCustom = custom; customSubscribers.forEach((update) => update()); }; // Track whether we're still in the initial render phase // This is true only when initial={false} and we haven't completed the first frame let isInitialRenderPhase = context.initial === false; // Track keys that have been seen (registered at least once) const seenKeys = new Set(); // Track keys that have exited (unregistered after being registered) const exitedKeys = new Set(); // For mode='wait': track whether enters should be blocked let enterBlocked = false; // For mode='wait': callbacks to invoke when enters are unblocked const enterUnblockedCallbacks = new Set(); // After first frame, mark initial render phase as complete // Guard for SSR - requestAnimationFrame only exists in browser if (isInitialRenderPhase && typeof window !== 'undefined') { requestAnimationFrame(() => { requestAnimationFrame(() => { pwLog('[presence] initial render phase complete, enabling animations for new keys'); isInitialRenderPhase = false; }); }); } /** * Determine if a child with the given key should animate its enter. * * - If we're past the initial render phase → always animate * - If key has previously exited → animate (re-entry) * - If key has never been seen AND we're in initial render phase → skip animation */ const shouldAnimateEnter = (key) => { // If the key has previously exited, it's a re-entry - always animate if (exitedKeys.has(key)) { pwLog('[presence] shouldAnimateEnter', { key, result: true, reason: 're-entry after exit' }); return true; } // If we're past the initial render phase, all new entries animate if (!isInitialRenderPhase) { pwLog('[presence] shouldAnimateEnter', { key, result: true, reason: 'past initial render phase' }); return true; } // We're in initial render phase and key hasn't exited before // Check if key has been seen - if not, skip animation (initial={false} behavior) const hasBeenSeen = seenKeys.has(key); const shouldAnimate = hasBeenSeen; // Only animate if we've seen it before (shouldn't happen in initial phase) pwLog('[presence] shouldAnimateEnter', { key, result: shouldAnimate, reason: shouldAnimate ? 'previously seen' : 'first appearance during initial render' }); return shouldAnimate; }; /** * Check if enter animations should be blocked. * * For mode='wait': returns true when exit animations are in progress, * signaling that new elements should defer their enter animations until * all exits complete. For 'sync' and 'popLayout' modes, always returns false. * * @returns True if enters should be blocked (wait mode with exits in progress) * @example * ```ts * if (context.isEnterBlocked()) { * // Defer animation until unblocked * context.onEnterUnblocked(() => runAnimation()) * } * ``` */ const isEnterBlocked = (key) => { if (mode !== 'wait') { pwLog('[presence] isEnterBlocked', { blocked: false, mode, inFlightExits, key }); return false; } const hasBlockingSibling = key !== undefined && Array.from(children.keys()).some((childKey) => childKey !== key); const blocked = inFlightExits > 0 || hasBlockingSibling; pwLog('[presence] isEnterBlocked', { blocked, mode, inFlightExits, key, hasBlockingSibling }); return blocked; }; /** * Register a callback to be invoked when enter animations are unblocked. * * For mode='wait': the callback is called when all exit animations complete * and new elements can begin their enter animations. Useful for deferring * animations until the appropriate time. * * @param callback - Function to call when enters are unblocked * @returns Unsubscribe function to remove the callback * @example * ```ts * const unsubscribe = context.onEnterUnblocked(() => { * console.log('Exits complete, starting enter animation') * runAnimation() * }) * // Later, to cancel: * unsubscribe() * ``` */ const onEnterUnblocked = (callback) => { pwLog('[presence] onEnterUnblocked: registering callback'); enterUnblockedCallbacks.add(callback); return () => { pwLog('[presence] onEnterUnblocked: removing callback'); enterUnblockedCallbacks.delete(callback); }; }; /** * Invoke all registered enter-unblocked callbacks. * * Called internally when all exit animations complete in wait mode. * Each callback is invoked in a try-catch to prevent one failing callback * from blocking others. * * @internal */ const notifyEnterUnblocked = () => { pwLog('[presence] notifyEnterUnblocked', { callbackCount: enterUnblockedCallbacks.size }); // Copy and clear to prevent re-invocation on multiple exit completions const callbacks = Array.from(enterUnblockedCallbacks); enterUnblockedCallbacks.clear(); callbacks.forEach((cb) => { try { cb(); } catch (e) { console.error('[presence] onEnterUnblocked callback error:', e); } }); }; pwLog('[presence] createContext', { initial, mode, isInitialRenderPhase, onExitComplete: !!context.onExitComplete }); const children = new Map(); // Track number of in-flight exit animations to invoke onExitComplete once let inFlightExits = 0; /** * Begin tracking an exit. * * Increments the `inFlightExits` counter and, in `mode='wait'`, raises the * `enterBlocked` flag so sibling motion-element enters defer until every * exit reports back via {@link finishExit}. Shared by the clone-based exit * path in {@link unregisterChild} and the user-driven `PresenceChild` hold. * * Must be paired with exactly one {@link finishExit} call per invocation. * * @returns void * @example * ```ts * // unregisterChild (clone path) * startExit() * requestAnimationFrame(() => { * animate(clone, exitKeyframes, transition).finished.finally(finishExit) * }) * * // PresenceChild (user-driven path) — exposed as `notifyExitStart` * presenceContext.notifyExitStart() * // ... later, on transitionend or user signal ... * presenceContext.notifyExitComplete() * ``` */ const startExit = () => { if (mode === 'wait') { enterBlocked = true; } inFlightExits += 1; }; /** * Mark an exit as finished. * * Decrements the `inFlightExits` counter. When the count reaches zero, * fires the consumer's `onExitComplete` callback and, in `mode='wait'`, * lowers `enterBlocked` plus notifies any deferred-enter callbacks * registered via {@link onEnterUnblocked}. * * Must be called exactly once per matching {@link startExit}; double-fires * underflow the counter and can permanently mis-route subsequent exits. * * @returns void * @example * ```ts * startExit() * // ... exit work ... * finishExit() // fires onExitComplete if the last exit, unblocks waiters * ``` */ const finishExit = () => { inFlightExits -= 1; if (inFlightExits === 0) { context.onExitComplete?.(); if (mode === 'wait' && enterBlocked) { enterBlocked = false; notifyEnterUnblocked(); } } }; /** * Register a child element and snapshot its initial rect/styles. */ const registerChild = (key, element, exit, mergedTransition, resolveExit) => { const initialRect = element.getBoundingClientRect(); const initialStyle = getComputedStyle(element); // Mark this key as seen const wasExited = exitedKeys.has(key); seenKeys.add(key); // If this key was previously exited, remove it from exitedKeys (it's re-entering) if (wasExited) { exitedKeys.delete(key); } // Note: For mode='wait', we do NOT preemptively block enters here. // Blocking only happens when an exit actually starts (in unregisterChild). // This ensures pure additions don't stall when other children merely have // exit definitions but aren't actually exiting. pwLog('[presence] registerChild', { key, hasExit: !!exit, exit, wasExited, mode, enterBlocked, rect: { w: initialRect.width, h: initialRect.height } }); children.set(key, { element, exit, resolveExit, mergedTransition, lastRect: initialRect, lastComputedStyle: initialStyle, lastPopLayoutSnapshot: mode === 'popLayout' ? measurePopLayoutSnapshot(element, initialStyle) : undefined, layoutInsertion: findLayoutInsertionParent(element) ?? undefined }); }; /** * Update the last known rect/style snapshot for a registered child. */ const updateChildState = (key, rect, computedStyle) => { const child = children.get(key); if (child && rect.width > 0 && rect.height > 0) { child.lastRect = rect; child.lastComputedStyle = computedStyle; if (mode === 'popLayout') { child.lastPopLayoutSnapshot = measurePopLayoutSnapshot(child.element, computedStyle); } } }; /** * Update the last captured mid-animation style values for a child. * Called from a rAF loop while WAAPI animations are running. */ const updateChildAnimatedStyle = (key, opacity, transform) => { const child = children.get(key); if (child) { child.lastAnimatedOpacity = opacity; child.lastAnimatedTransform = transform; } }; /** * Unregister a child. If it has an `exit` definition, create a styled * clone and run the exit animation using Motion. Cleans up after finish. */ const unregisterChild = (key) => { const child = children.get(key); pwLog('[presence] unregisterChild', { key, mode, found: !!child, hasExit: !!child?.exit, exit: child?.exit }); // Only process if child was actually registered if (!child) { pwLog('[presence] unregisterChild - child not found, ignoring'); return; } // Mark this key as exited so re-entry will animate exitedKeys.add(key); if (!child.exit && !child.resolveExit) { pwLog('[presence] unregisterChild - no exit animation, removing immediately'); children.delete(key); return; } const rect = child.lastRect; const computed = child.lastComputedStyle; // Wait mode holds the exiting layout slot while the entering child is hidden. // Sync and popLayout must not add an extra layout participant. const shouldPreserveLayout = mode === 'wait'; let placeholder = null; const liveLayoutInsertion = findLayoutInsertionParent(child.element); const layoutInsertion = liveLayoutInsertion ?? (child.layoutInsertion?.parent.isConnected ? child.layoutInsertion : null); if (shouldPreserveLayout && layoutInsertion) { placeholder = document.createElement(child.element.tagName.toLowerCase()); placeholder.setAttribute('data-presence-placeholder', 'true'); placeholder.style.display = computed.display === 'contents' ? 'block' : computed.display; placeholder.style.width = `${rect.width}px`; placeholder.style.height = `${rect.height}px`; placeholder.style.margin = computed.margin; placeholder.style.boxSizing = computed.boxSizing; placeholder.style.position = 'static'; placeholder.style.visibility = 'hidden'; placeholder.style.pointerEvents = 'none'; if (computed.flex) { placeholder.style.flex = computed.flex; } if (computed.alignSelf) { placeholder.style.alignSelf = computed.alignSelf; } if (computed.gridColumnStart) { placeholder.style.gridColumnStart = computed.gridColumnStart; } if (computed.gridColumnEnd) { placeholder.style.gridColumnEnd = computed.gridColumnEnd; } if (computed.gridRowStart) { placeholder.style.gridRowStart = computed.gridRowStart; } if (computed.gridRowEnd) { placeholder.style.gridRowEnd = computed.gridRowEnd; } const before = layoutInsertion.before?.parentElement === layoutInsertion.parent ? layoutInsertion.before : null; layoutInsertion.parent.insertBefore(placeholder, before); } // Clone original node to preserve structure/classes, then inline computed styles to freeze look const clone = child.element.cloneNode(true); if (clone.id) clone.removeAttribute('id'); try { for (let i = 0; i < computed.length; i += 1) { const prop = computed[i]; // Skip transforms to avoid double offset/scale on the absolutely positioned clone if (/transform/i.test(prop)) continue; const value = computed.getPropertyValue(prop); const priority = computed.getPropertyPriority(prop); if (value) clone.style.setProperty(prop, value, priority); } // Ensure no transform remains on the clone (including vendor-prefixed) resetTransforms(clone); } catch { // Ignore } // Apply last captured mid-animation values (from rAF polling) so that // exit clones start from the correct visual state when interrupting // an enter animation. The element is disconnected by now so // getComputedStyle/getAnimations won't reflect in-flight values. if (child.lastAnimatedOpacity != null) { clone.style.opacity = child.lastAnimatedOpacity; } // Attach to the original insertion parent and position absolutely at the // last known rect. Svelte can detach keyed nodes before unregister runs, // so fall back to the parent captured at registration time instead of // escaping to <body>, which would bypass clipping parents. let parent = child.element.parentElement ?? (child.layoutInsertion?.parent.isConnected ? child.layoutInsertion.parent : null) ?? document.body; let positioningParent = parent; // Walk up to find a parent that has actual layout (not display: contents) while (positioningParent && positioningParent !== document.body) { const parentDisplay = getComputedStyle(positioningParent).display; if (parentDisplay !== 'contents') { break; } positioningParent = positioningParent.parentElement ?? document.body; } const popLayoutSnapshot = mode === 'popLayout' ? child.element.isConnected ? measurePopLayoutSnapshot(child.element, computed) : child.lastPopLayoutSnapshot : undefined; const parentRect = popLayoutSnapshot ? undefined : positioningParent.getBoundingClientRect(); if (!popLayoutSnapshot) { const parentPosition = getComputedStyle(positioningParent).position; if (parentPosition === 'static') { ; positioningParent.style.position = 'relative'; } } // Append to the actual positioning parent parent = positioningParent; // Preserve the original display property (especially flex for centered content) const originalDisplay = computed.display; clone.style.left = ''; clone.style.right = ''; clone.style.top = ''; clone.style.bottom = ''; if (popLayoutSnapshot) { const popStyles = resolvePopLayoutStyles(popLayoutSnapshot); if (popStyles.position) clone.style.position = popStyles.position; if (popStyles.width) clone.style.width = popStyles.width; if (popStyles.height) clone.style.height = popStyles.height; if (popStyles.left) clone.style.left = popStyles.left; if (popStyles.right) clone.style.right = popStyles.right; if (popStyles.top) clone.style.top = popStyles.top; if (popStyles.bottom) clone.style.bottom = popStyles.bottom; } else { clone.style.position = 'absolute'; clone.style.top = `${rect.top - parentRect.top + (parent.scrollTop ?? 0)}px`; clone.style.left = `${rect.left - parentRect.left + (parent.scrollLeft ?? 0)}px`; clone.style.width = `${rect.width}px`; clone.style.height = `${rect.height}px`; } clone.style.pointerEvents = 'none'; clone.style.visibility = 'visible'; // Preserve flex/grid layout, only force 'block' if it was 'none' or 'contents' if (originalDisplay === 'none' || originalDisplay === 'contents') { clone.style.display = 'block'; } if (!popLayoutSnapshot) { clone.style.margin = '0'; } clone.style.boxSizing = 'border-box'; // Redundantly ensure no transforms are applied before positioning/z-index take effect resetTransforms(clone); // Elevate clone above siblings to ensure it renders on top during exit try { const siblings = Array.from(parent.children); let maxZ = 0; for (const sib of siblings) { if (sib === clone) continue; const z = parseInt(getComputedStyle(sib).zIndex || '0', 10); if (!Number.isNaN(z)) maxZ = Math.max(maxZ, z); } // Ensure positioned so z-index applies; already absolute above clone.style.zIndex = String(maxZ + 1 || 9999); } catch { clone.style.zIndex = '9999'; } clone.setAttribute('data-clone', 'true'); clone.setAttribute('data-exiting', 'true'); clone.setAttribute('data-mode', mode); pwLog('[presence] clone created', { key, mode, rect: { w: rect.width, h: rect.height, top: rect.top, left: rect.left } }); parent.appendChild(clone); // Capture the element reference for this specific exit animation // This prevents race conditions where re-entry registers a new element with the same key // before this exit animation completes const exitingElement = child.element; requestAnimationFrame(() => { const resolvedExit = child.resolveExit?.(getCustom()) ?? child.exit; if (!resolvedExit) { pwLog('[presence] unregisterChild - no resolved exit animation after custom update'); clone.remove(); placeholder?.remove(); const currentChild = children.get(key); if (currentChild && currentChild.element === exitingElement) { children.delete(key); } return; } // Prepare exit keyframes - extract ease separately, filter out transition // Note: transition is filtered out here as it's accessed via exitObj.transition for merging const rawExit = (resolvedExit ?? {}); const { ease: exitEase, transition: __, ...exitKeyframes } = rawExit; void __; // Suppress unused variable warning - transition is accessed via exitObj.transition // Merge transitions: default < mergedTransition < exit.transition < exit.ease (last wins) const exitObj = (resolvedExit ?? {}); const finalTransition = mergeTransitions({ duration: 0.35 }, (child.mergedTransition ?? {}), (exitObj.transition ?? {}), exitEase ? { ease: exitEase } : {}); pwLog('[presence] starting exit animation', { key, mode, exitKeyframes, finalTransition }); // Start exit and track in-flight count (handles wait-mode blocking) startExit(); animate(clone, exitKeyframes, finalTransition) .finished.catch(() => { }) .finally(() => { pwLog('[presence] exit animation complete', { key, mode }); // Reset elevated styles then remove try { clone.style.zIndex = ''; } catch { // ignore } clone.remove(); // Log clone removal and element counts for debugging rapid toggle pwLog('[presence] clone REMOVED from DOM', { key, mode, clonesInDOM: document.querySelectorAll('[data-clone="true"]').length, boxesInDOM: document.querySelectorAll('[data-testid="box"]').length }); // Only delete from children map if the current registration is for the SAME element // If a re-entry happened while we were animating, a new element is registered // and we should NOT delete it const currentChild = children.get(key); if (currentChild && currentChild.element === exitingElement) { children.delete(key); pwLog('[presence] child deleted from map (same element)', { key }); } else { pwLog('[presence] child NOT deleted (re-entry registered new element)', { key, hasCurrentChild: !!currentChild, isSameElement: currentChild?.element === exitingElement }); } // Log final state pwLog('[presence] element count after exit', { childrenMapSize: children.size, inFlightExits: inFlightExits - 1, clonesInDOM: document.querySelectorAll('[data-clone="true"]').length }); placeholder?.remove(); finishExit(); }); }); }; return { initial, mode, get custom() { trackCustom(); return getCustom(); }, getCustom, setCustom, shouldAnimateEnter, isEnterBlocked, onEnterUnblocked, onExitComplete: context.onExitComplete, registerChild, updateChildState, updateChildAnimatedStyle, unregisterChild, notifyExitStart: startExit, notifyExitComplete: finishExit }; }; /** * Get the current `AnimatePresence` context from Svelte component context. * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next 3 */ export const getAnimatePresenceContext = () => { return getContext(ANIMATE_PRESENCE_CONTEXT); }; /** * Set the `AnimatePresence` context into Svelte component context. * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next 3 */ export const setAnimatePresenceContext = (context) => { setContext(ANIMATE_PRESENCE_CONTEXT, context); }; /** * Get the current presence depth from Svelte component context. * * Returns undefined if not inside an AnimatePresence, or the depth level * where 0 means direct child of AnimatePresence. * * @returns The current depth level (0 for direct children), or undefined if outside AnimatePresence. * @example * ```ts * const depth = getPresenceDepth() * if (depth === 0) { * // Direct child of AnimatePresence - key prop required * } * ``` * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next */ export const getPresenceDepth = () => getContext(PRESENCE_DEPTH_CONTEXT); /** * Set the presence depth in Svelte component context. * * AnimatePresence sets this to 0, and each motion element increments it * for its descendants so only direct children (depth 0) require keys. * * @param depth - The nesting depth to set (0 for direct children of AnimatePresence). * @returns void * @example * ```ts * // In AnimatePresence component * setPresenceDepth(0) * * // In nested motion element * const currentDepth = getPresenceDepth() ?? 0 * setPresenceDepth(currentDepth + 1) * ``` * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next */ export const setPresenceDepth = (depth) => { setContext(PRESENCE_DEPTH_CONTEXT, depth); }; const PRESENCE_CHILD_CONTEXT = Symbol('presence-child-context'); /** * Get the nearest `PresenceChild` context from Svelte component context, or * `undefined` if the caller is not wrapped in one. * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next 3 */ export const getPresenceChildContext = () => { return getContext(PRESENCE_CHILD_CONTEXT); }; /** * Install a `PresenceChild` context for descendants. * * Note: Trivial wrapper - ignored for coverage. */ /* c8 ignore next 3 */ export const setPresenceChildContext = (context) => { setContext(PRESENCE_CHILD_CONTEXT, context); };