@tamagui/animations-reanimated 3.0.14 → 3.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/createAnimations.cjs +5 -1
- package/dist/cjs/createAnimations.native.cjs +5 -1
- package/dist/cjs/createAnimations.native.js +5 -1
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/esm/createAnimations.mjs +5 -1
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +5 -1
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/package.json +7 -7
- package/src/createAnimations.tsx +10 -1
- package/types/createAnimations.d.ts.map +1 -1
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
],
|
|
7
7
|
"version": 3,
|
|
8
8
|
"sourcesContent": [
|
|
9
|
-
"import {\n easingToBezier,\n forAnimationState,\n getTransitionForKey,\n hasTransition,\n resolveTransition,\n type ResolvedEntry,\n type ResolvedTransition,\n type AnimationsConfig,\n} from '@tamagui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useEvent,\n useIsomorphicLayoutEffect,\n View,\n type AnimationDriverWithAnimatedNumbers,\n type NativeTextMetrics,\n type TransitionProp,\n type UniversalAnimatedNumber,\n createRefComponent,\n} from '@tamagui/core'\nimport { useThemeWithState } from '@tamagui/web/internal-runtime'\nimport { TextInputHost } from './textInputHost'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport normalizeColor from '@react-native/normalize-colors'\nimport React, { useMemo, useRef } from 'react'\nimport type { SharedValue } from 'react-native-reanimated'\nimport Animated_, {\n cancelAnimation,\n Easing,\n runOnJS,\n runOnUI,\n useAnimatedReaction,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withDelay,\n withSpring,\n withTiming,\n type WithSpringConfig,\n type WithTimingConfig,\n} from 'react-native-reanimated'\n\n// sizing keywords and calc() have no numeric endpoint and cannot gate exits\nconst NON_ANIMATABLE_VALUE =\n /^(auto|min-content|max-content|fit-content|stretch|inherit|initial|unset)\\b|^calc\\(/\n\n// =============================================================================\n// ESM/CJS compatibility\n// =============================================================================\n\n/**\n * Handle ESM/CJS module interop for react-native-reanimated\n * @see https://github.com/evanw/esbuild/issues/2480#issuecomment-1833104754\n */\nconst getDefaultExport = <T,>(module: T | { default: T }): T => {\n const mod = module as any\n if (mod.__esModule || mod[Symbol.toStringTag] === 'Module') {\n return mod.default || mod\n }\n return mod\n}\n\nconst Animated = getDefaultExport(Animated_)\n\nconst silenceAnimatedComponentDevCheck = (style: unknown) => {\n if (\n process.env.NODE_ENV !== 'development' ||\n isWeb ||\n !style ||\n typeof style !== 'object'\n ) {\n return\n }\n\n // react fabric's dev performance logger diffs component props and reads the\n // reanimated guard getter even when the style is on an animated component.\n Object.defineProperty(style, '_requiresAnimatedComponent', {\n configurable: true,\n enumerable: false,\n value: true,\n })\n}\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype ReanimatedAnimatedNumber = SharedValue<number>\n\ntype AnimationSnapshot = {\n animated: Record<string, unknown>\n statics: Record<string, unknown>\n transforms: Array<Record<string, unknown>>\n gatedKeys: Record<string, boolean>\n /**\n * the leading is a ratio of a font size that can move under it, so the mapper\n * multiplies the two live factors instead of animating a lineHeight of its\n * own. false for an absolute length, for `normal`, and for a font size that\n * is already at rest.\n */\n derivesLeading: boolean\n /**\n * for each animated key (transform sub-keys as `transform:key`), the value the\n * previous committed render painted for it, when one exists and is animatable.\n * the worklet animates a mapper-fresh key FROM its seed instead of snapping —\n * this is how an enter-clause value painted statically during mount becomes the\n * start point once the key turns animated. keys with no seed paint their target\n * directly (a key appearing from nothing has nothing to animate from).\n */\n seeds: Record<string, unknown>\n removedKeys: Record<string, boolean>\n removeTransform: boolean\n clearValue: '' | null\n}\n\n/** Spring animation configuration */\ntype SpringConfig = {\n type?: 'spring'\n delay?: number\n} & Partial<WithSpringConfig>\n\n/** Timing animation configuration */\ntype TimingConfig = {\n type: 'timing'\n delay?: number\n} & Partial<WithTimingConfig>\n\n/** Combined animation configuration type */\nexport type TransitionConfig = SpringConfig | TimingConfig\n\n/** Options for createAnimations (reserved for future use) */\nexport type CreateAnimationsOptions = {}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Resolve dynamic theme values (e.g., `{dynamic: {dark: \"value\", light: \"value\"}}`)\n */\nconst resolveDynamicValue = (value: unknown, isDark: boolean): unknown => {\n if (\n value !== null &&\n typeof value === 'object' &&\n 'dynamic' in value &&\n typeof (value as any).dynamic === 'object'\n ) {\n const dynamic = (value as any).dynamic\n return isDark ? dynamic.dark : dynamic.light\n }\n return value\n}\n\n/** Animation completion callback type */\ntype AnimationCallback = (finished?: boolean) => void\n\ntype MapperState = {\n emitted: Record<string, boolean>\n // static keys a style emitter has written through the mapper. nothing else\n // may emit statics: reanimated snapshots this worklet's first-render output\n // per style handle and re-applies that snapshot under every later style\n // prop, so a static emitted on mount (a disabled mount's pointerEvents:\n // 'none', its opacity) would outlive the render that removed it. render\n // statics reach the view through the staticStyles element instead.\n ownedStatics: Record<string, boolean>\n}\n\nconst updateMapperState = isWeb\n ? (\n state: SharedValue<MapperState>,\n emitted: Record<string, boolean>,\n ownedStatics: Record<string, boolean>\n ) => {\n state.value.emitted = emitted\n state.value.ownedStatics = ownedStatics\n }\n : (\n state: SharedValue<MapperState>,\n emitted: Record<string, boolean>,\n ownedStatics: Record<string, boolean>\n ) => {\n 'worklet'\n if (!globalThis._WORKLET) return\n state.modify((current) => {\n current.emitted = emitted\n current.ownedStatics = ownedStatics\n return current\n }, false)\n }\n\nconst cloneAnimationValue = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(cloneAnimationValue)\n }\n\n if (value && typeof value === 'object') {\n const next: Record<string, unknown> = {}\n for (const key in value as Record<string, unknown>) {\n next[key] = cloneAnimationValue((value as Record<string, unknown>)[key])\n }\n return next\n }\n\n return value\n}\n\nconst cloneStyleRecord = (style: Record<string, unknown>): Record<string, unknown> => {\n const next: Record<string, unknown> = {}\n for (const key in style) {\n next[key] = cloneAnimationValue(style[key])\n }\n return next\n}\n\nconst cloneTransitionConfig = (config: TransitionConfig): TransitionConfig => {\n return cloneAnimationValue(config) as TransitionConfig\n}\n\nconst getAnimatedTransforms = (value: unknown): Array<Record<string, unknown>> => {\n if (!Array.isArray(value)) return []\n return value.filter(\n (item): item is Record<string, unknown> =>\n item !== null && typeof item === 'object' && !Array.isArray(item)\n )\n}\n\nconst getRemovedAnimatedKeys = (\n current: Set<string>,\n previous: Set<string>\n): Record<string, boolean> => {\n const removedKeys: Record<string, boolean> = {}\n for (const key of previous) {\n if (!current.has(key)) removedKeys[key] = true\n }\n return removedKeys\n}\n\n// implicit start values for style keys that were never painted (e.g. a key\n// introduced by an exit clause with no base value). most numerics default to 0.\nconst getImplicitDefault = (\n key: string,\n targetValue: number | string\n): number | string => {\n 'worklet'\n const value =\n key === 'opacity' || key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0\n if (typeof targetValue !== 'string') return value\n\n // reanimated derives a string animation's suffix from its start value. keep\n // units on implicit starts so fresh `50%` and `90deg` keys interpolate.\n let suffixIndex = 0\n while (suffixIndex < targetValue.length) {\n const character = targetValue[suffixIndex]\n const code = character.charCodeAt(0)\n if (\n (code >= 48 && code <= 57) ||\n character === '.' ||\n character === '-' ||\n character === '+'\n ) {\n suffixIndex++\n } else {\n break\n }\n }\n return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value\n}\n\n// A numeric authored lineHeight is a RATIO of the resolved font size, not a\n// length. Core finalizes the destination to fontSize * ratio and reports the\n// ratio back on `nativeTextMetrics`, so the painted leading has to be\n// fontSize * ratio on EVERY frame, not only at the destination. An absolute px\n// leading, and any raw react-native style, keeps its own value and its own\n// timeline.\nconst getLeadingRatio = (metrics: { lineHeight?: unknown } | undefined | null) => {\n const ratio = metrics?.lineHeight\n return typeof ratio === 'number' && Number.isFinite(ratio) && ratio >= 0\n ? ratio\n : undefined\n}\n\n// The mapper cannot multiply two reanimated DESCRIPTORS, but it can multiply\n// two live numbers: the font size this node paints is mirrored into a shared\n// value frame by frame (see applyAnimation) and the ratio rides a shared value\n// of its own, so the leading is their product, recomputed on every frame either\n// factor moves. That is what makes a font size and a ratio changing together\n// paint fs(t) * r(t) instead of a ramp to fs_end * r_end.\n//\n// A transition covering neither key leaves both to land in the same commit,\n// already coherent, so there is nothing to derive. An inherited font size runs\n// on the ancestor's clock and never has an entry here, so it always derives.\nconst derivesLeadingProduct = (\n style: Record<string, unknown>,\n ratio: number | undefined,\n inheritsFontSize: boolean,\n resolved: ResolvedTransition\n) =>\n ratio !== undefined &&\n (inheritsFontSize ||\n (typeof style.lineHeight === 'number' &&\n typeof style.fontSize === 'number' &&\n (!!getTransitionForKey(resolved, 'fontSize') ||\n !!getTransitionForKey(resolved, 'lineHeight'))))\n\nconst COLOR_STYLE_KEYS: Record<string, boolean> = {\n backgroundColor: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n color: true,\n}\n\n// reanimated can pass either its rgba output or a processed numeric color back\n// as animation history. snapshot targets are normalized on the JS side below;\n// this worklet conversion exists only to validate that in-flight history.\nconst toReanimatedColor = (value: unknown): unknown => {\n 'worklet'\n if (typeof value === 'number') {\n const color = value >>> 0\n const alpha = ((color >>> 24) & 255) / 255\n const red = (color >>> 16) & 255\n const green = (color >>> 8) & 255\n const blue = color & 255\n return `rgba(${red}, ${green}, ${blue}, ${alpha})`\n }\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n if (trimmed === 'transparent') return 'rgba(0, 0, 0, 0)'\n return trimmed\n}\n\nconst isReanimatedColorHistory = (value: unknown): boolean => {\n 'worklet'\n const normalized = toReanimatedColor(value)\n return typeof normalized === 'string' && normalized.startsWith('rgba(')\n}\n\nconst normalizeAnimationColor = (value: unknown): string | undefined => {\n if (typeof value === 'number') {\n return toReanimatedColor(value) as string\n }\n if (typeof value !== 'string') return\n\n const color = normalizeColor(value)\n if (color == null) return\n const red = Math.round((color & 0xff000000) >>> 24)\n const green = Math.round((color & 0x00ff0000) >>> 16)\n const blue = Math.round((color & 0x0000ff00) >>> 8)\n const alpha = (color & 0x000000ff) / 255\n return `rgba(${red}, ${green}, ${blue}, ${alpha})`\n}\n\nconst buildSnapshot = (\n animated: Record<string, unknown>,\n statics: Record<string, unknown>,\n transforms: Array<Record<string, unknown>>,\n previousKeys: Set<string>,\n lastPainted: Record<string, unknown>,\n derivesLeading = false\n): {\n value: AnimationSnapshot\n painted: Record<string, unknown>\n keys: Set<string>\n} => {\n const snapshotAnimated: Record<string, unknown> = {}\n const snapshotStatics = cloneStyleRecord(statics)\n\n for (const key in snapshotStatics) {\n if (!COLOR_STYLE_KEYS[key]) continue\n const normalized = normalizeAnimationColor(snapshotStatics[key])\n if (normalized !== undefined) snapshotStatics[key] = normalized\n }\n\n for (const key in animated) {\n if (key === 'transform') continue\n const value = animated[key]\n if (COLOR_STYLE_KEYS[key]) {\n const normalized = normalizeAnimationColor(value)\n if (normalized === undefined) {\n // invalid colors must paint plainly. creating a descriptor makes\n // reanimated's string interpolator emit '<letters>NaN'.\n snapshotStatics[key] = cloneAnimationValue(value)\n continue\n }\n snapshotAnimated[key] = normalized\n } else {\n snapshotAnimated[key] = cloneAnimationValue(value)\n }\n }\n\n const snapshotTransforms = transforms.map(cloneStyleRecord)\n const keys = new Set(Object.keys(snapshotAnimated))\n if (derivesLeading) keys.add('lineHeight')\n for (const transform of snapshotTransforms) {\n const key = Object.keys(transform)[0]\n const value = key ? transform[key] : undefined\n if (key && (typeof value === 'number' || typeof value === 'string')) {\n keys.add(`transform:${key}`)\n }\n }\n\n const seeds: Record<string, unknown> = {}\n const staticTransforms = getAnimatedTransforms(snapshotStatics.transform)\n for (const transform of staticTransforms) {\n const key = Object.keys(transform)[0]\n if (keys.has(`transform:${key}`) && previousKeys.has(`transform:${key}`))\n seeds[`transform:${key}`] = transform[key]\n }\n for (const key of keys) {\n // React carries the first animated value until the mapper owns the key.\n // later commits can coalesce before its first run, so their destinations\n // are not evidence of a painted predecessor.\n let value = previousKeys.has(key)\n ? (seeds[key] ?? snapshotStatics[key] ?? lastPainted[key])\n : lastPainted[key]\n if (typeof value !== 'number' && typeof value !== 'string') {\n delete seeds[key]\n continue\n }\n if (typeof value === 'string' && NON_ANIMATABLE_VALUE.test(value)) {\n delete seeds[key]\n continue\n }\n if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value)\n if (value !== undefined) seeds[key] = value\n }\n\n const gatedKeys: Record<string, boolean> = {}\n for (const key of keys) gatedKeys[key] = true\n // a derived leading is a product the mapper computes, not an animation with a\n // completion callback, so no enter/exit cycle can wait on it\n if (derivesLeading) delete gatedKeys.lineHeight\n\n const removedKeys = getRemovedAnimatedKeys(keys, previousKeys)\n const painted: Record<string, unknown> = {\n ...snapshotStatics,\n ...snapshotAnimated,\n }\n painted.transform = snapshotTransforms.length ? snapshotTransforms : staticTransforms\n for (const transform of staticTransforms) {\n const key = Object.keys(transform)[0]\n if (key) painted[`transform:${key}`] = transform[key]\n }\n for (const transform of snapshotTransforms) {\n const key = Object.keys(transform)[0]\n if (key) painted[`transform:${key}`] = transform[key]\n }\n\n return {\n value: {\n animated: snapshotAnimated,\n statics: snapshotStatics,\n transforms: snapshotTransforms,\n gatedKeys,\n derivesLeading,\n seeds,\n removedKeys,\n removeTransform: Object.keys(removedKeys).some((key) =>\n key.startsWith('transform:')\n ),\n clearValue: isWeb ? '' : null,\n },\n painted,\n keys,\n }\n}\n\nconst getGatedKeys = (snapshot: AnimationSnapshot): string[] =>\n Object.keys(snapshot.gatedKeys)\n\nconst createReanimatedConfig = (config: TransitionConfig): Record<string, unknown> => {\n 'worklet'\n const next: Record<string, unknown> = {}\n const source = config as Record<string, unknown>\n\n for (const key in source) {\n if (key === 'type' || key === 'delay') continue\n const value = source[key]\n if (value !== undefined) {\n next[key] = value\n }\n }\n\n return next\n}\n\n/**\n * Apply animation to a value based on config, with optional completion callback\n */\nconst applyAnimation = <T extends number | string>(\n targetValue: T,\n config: TransitionConfig,\n callback?: AnimationCallback,\n seedValue?: number | string,\n validateStartAsColor = false,\n publishTo?: SharedValue<number>\n): T => {\n 'worklet'\n const delay = config.delay\n const reanimatedConfig = createReanimatedConfig(config)\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(\n targetValue as number,\n reanimatedConfig as WithTimingConfig,\n callback\n )\n } else {\n animatedValue = withSpring(\n targetValue as number,\n reanimatedConfig as WithSpringConfig,\n callback\n )\n\n // reanimated stamps lastTimestamp from performance.now() when an animation\n // starts outside a frame (an event handler), then steps it on the frame\n // timestamp, which is the frame's start and lags performance.now() badly\n // when frames are starved. the delta goes negative, and the closed-form\n // spring exponentiates instead of decaying: a tooltip crossing triggers on\n // a loaded machine lands at translate -33,554,430px for a frame before it\n // relaxes back. reanimated clamps the delta's upper bound only, so hold the\n // clock monotonic before it reaches the spring math.\n const innerOnFrame = animatedValue.onFrame\n animatedValue.onFrame = (animation: any, now: number) => {\n 'worklet'\n return innerOnFrame(\n animation,\n now < animation.lastTimestamp ? animation.lastTimestamp : now\n )\n }\n }\n\n // reanimated starts a descriptor from its per-view history for the key — the\n // key's last output value, or the descriptor's own toValue when there is\n // none (an instant snap). both are wrong for a key that turns animated with\n // a painted predecessor: history may hold a stale clear-value ('') from when\n // the key was last removed, which poisons the start value (NaN frames). the\n // wrapper substitutes the seed for whatever start value reanimated passes.\n // color history gets the same treatment only when reanimated's own parser\n // rejects it, preserving valid in-flight values during interruption.\n //\n // guard: when reanimated's IN_STYLE_UPDATER flag is stuck true (e.g. after\n // a throw during initialUpdaterRun), withSpring/withTiming return the raw\n // target value (number or string) instead of an animation descriptor.\n // bail out instead of crashing on .onStart assignment. (#4193)\n //\n // note: we avoid `typeof x === 'object'` here because SWC's ES5 transform\n // replaces it with a `_type_of()` helper that isn't a worklet — calling it\n // on the UI Runtime crashes Reanimated 4. checking for known primitive types\n // instead is safe because SWC leaves those comparisons as plain `typeof`.\n const isAnimationDescriptor =\n animatedValue !== null &&\n typeof animatedValue !== 'number' &&\n typeof animatedValue !== 'string' &&\n typeof animatedValue !== 'boolean' &&\n typeof animatedValue !== 'undefined'\n\n if (isAnimationDescriptor && delay && delay > 0) {\n animatedValue = withDelay(delay, animatedValue)\n }\n\n if (isAnimationDescriptor && (seedValue !== undefined || validateStartAsColor)) {\n const innerOnStart = animatedValue.onStart\n animatedValue.onStart = (\n animation: unknown,\n value: unknown,\n timestamp: number,\n previousAnimation: unknown\n ) => {\n 'worklet'\n const startValue = validateStartAsColor\n ? isReanimatedColorHistory(value)\n ? toReanimatedColor(value)\n : (seedValue ?? targetValue)\n : seedValue\n innerOnStart(animation, startValue, timestamp, previousAnimation)\n }\n }\n\n // a published font size mirrors what THIS descriptor paints, frame by frame.\n // descendants that inherit the size read the mirror, so the host's own\n // animation stays the only clock: no shadow animation, no second config, and\n // nothing to keep in sync when this one is interrupted.\n // (withDelay already wrapped above, before the seed onStart override, per\n // main's hold-enterStyle-through-delay fix: applying it again here would\n // double the delay.)\n if (isAnimationDescriptor && publishTo) {\n const startedOn = animatedValue.onStart\n animatedValue.onStart = (\n animation: any,\n value: unknown,\n timestamp: number,\n previousAnimation: unknown\n ) => {\n 'worklet'\n startedOn(animation, value, timestamp, previousAnimation)\n publishTo.value = animation.current\n }\n const framedOn = animatedValue.onFrame\n animatedValue.onFrame = (animation: any, now: number) => {\n 'worklet'\n const finished = framedOn(animation, now)\n publishTo.value = animation.current\n return finished\n }\n }\n\n return animatedValue as T\n}\n\nconst animateSnapshotValue = (\n key: string,\n implicitKey: string,\n targetValue: number | string,\n config: TransitionConfig,\n previouslyEmitted: boolean,\n seedValue: unknown,\n gated: boolean,\n currentlyExiting: boolean,\n exitCycleId: number,\n currentlyCompletingEnter: boolean,\n enterCycleId: number,\n currentlyCompletingUpdate: boolean,\n updateCycleId: number,\n markExitKeyDone: (key: string, cycleId: number, finished: boolean) => void,\n markEnterKeyDone: (key: string, cycleId: number) => void,\n markUpdateKeyDone: (key: string, cycleId: number, finished: boolean) => void,\n validateStartAsColor = false,\n publishTo?: SharedValue<number>\n): number | string => {\n 'worklet'\n\n const cycleGated =\n gated && (currentlyExiting || currentlyCompletingEnter || currentlyCompletingUpdate)\n if (!previouslyEmitted && seedValue === undefined && !cycleGated) {\n // painted plainly, so the mirror takes it straight\n if (publishTo && typeof targetValue === 'number') publishTo.value = targetValue\n return targetValue\n }\n\n let callback: AnimationCallback | undefined\n if (gated && currentlyExiting) {\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false)\n }\n } else if (gated && currentlyCompletingEnter) {\n callback = () => {\n 'worklet'\n runOnJS(markEnterKeyDone)(key, enterCycleId)\n }\n } else if (gated && currentlyCompletingUpdate) {\n callback = (finished) => {\n 'worklet'\n runOnJS(markUpdateKeyDone)(key, updateCycleId, finished ?? false)\n }\n }\n\n return applyAnimation(\n targetValue,\n config,\n callback,\n previouslyEmitted\n ? undefined\n : ((seedValue ?? getImplicitDefault(implicitKey, targetValue)) as number | string),\n validateStartAsColor,\n publishTo\n )\n}\n\n// =============================================================================\n// Animatable Properties\n// =============================================================================\n\nconst ANIMATABLE_PROPERTIES: Record<string, boolean> = {\n // Transform\n transform: true,\n // Opacity\n opacity: true,\n // Dimensions\n height: true,\n width: true,\n minWidth: true,\n minHeight: true,\n maxWidth: true,\n maxHeight: true,\n // Background\n backgroundColor: true,\n // Border colors\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n // Border radius\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n // Border width\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n // Text\n color: true,\n fontSize: true,\n fontWeight: true,\n lineHeight: true,\n letterSpacing: true,\n // Position\n left: true,\n right: true,\n top: true,\n bottom: true,\n // Margin\n margin: true,\n marginTop: true,\n marginBottom: true,\n marginLeft: true,\n marginRight: true,\n marginHorizontal: true,\n marginVertical: true,\n // Padding\n padding: true,\n paddingTop: true,\n paddingBottom: true,\n paddingLeft: true,\n paddingRight: true,\n paddingHorizontal: true,\n paddingVertical: true,\n // Flex/Gap\n gap: true,\n rowGap: true,\n columnGap: true,\n flex: true,\n flexGrow: true,\n flexShrink: true,\n}\n\n/**\n * Check if a style property can be animated\n */\nconst canAnimateProperty = (key: string, value: unknown): boolean => {\n if (!ANIMATABLE_PROPERTIES[key]) return false\n if (typeof value === 'string' && NON_ANIMATABLE_VALUE.test(value)) return false\n return true\n}\n\n// narrowing to a subset of properties is the transition's own job: a key no\n// entry covers gets `instantConfig()` in `buildTransitionConfig` and snaps.\nconst splitAnimationStyles = (\n style: Record<string, unknown>,\n isDark: boolean,\n disableAnimation: boolean\n) => {\n const animated: Record<string, unknown> = {}\n const statics: Record<string, unknown> = {}\n\n for (const key in style) {\n const value = resolveDynamicValue(style[key], isDark)\n if (value === undefined) continue\n\n if (!disableAnimation && canAnimateProperty(key, value)) {\n animated[key] = cloneAnimationValue(value)\n } else {\n statics[key] = cloneAnimationValue(value)\n }\n }\n\n return { animated, statics }\n}\n\n// =============================================================================\n// Animated Components (Web)\n// =============================================================================\n\n/**\n * Create a Tamagui-compatible animated component for web\n * Supports data- attributes, className, and proper style handling\n */\nfunction createWebAnimatedComponent(defaultTag: 'div' | 'span') {\n const isText = defaultTag === 'span'\n\n const Component = Animated.createAnimatedComponent(\n createRefComponent((propsIn: any, ref) => {\n const { forwardedRef, render = defaultTag, ...rest } = propsIn\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)\n\n const stateRef = useRef<{ host: HTMLElement | null }>({\n get host() {\n return hostRef.current\n },\n })\n\n const [, themeState] = useThemeWithState({})\n\n const result = getSplitStyles(\n rest,\n isText ? Text.staticConfig : View.staticConfig,\n themeState?.theme ?? {},\n themeState?.name ?? '',\n { unmounted: false } as any,\n { isAnimated: false, noClass: true }\n )\n\n const viewProps = result?.viewProps ?? {}\n const Element = render\n const transformedProps = hooks.usePropsTransform?.(\n render,\n viewProps,\n stateRef as any,\n false\n )\n const { nativeID, ...webProps } = transformedProps ?? viewProps\n\n return <Element {...webProps} ref={composedRefs} />\n })\n )\n ;(Component as any).acceptRenderProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// a TextInput never inherits font size from an ancestor Text on native, so the\n// binding hook has to hand it a host that accepts animated styles of its own.\n// built on first use: the compiler evaluates tamagui.config.ts against a\n// react-native stub that has no TextInput, and never binds any text.\nlet animatedTextInput: React.ComponentType<any> | undefined\n\n// =============================================================================\n// Transition Config Builder\n// =============================================================================\n\ntype TransitionConfigResult = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n resolved: ResolvedTransition\n}\n\nconst defaultConfig: TransitionConfig = { type: 'spring' }\n\n// one Easing.bezier worklet per easing: a fresh worklet re-serializes to the\n// UI runtime every time configRef is written, and a render or emit rebuilds\n// every key's config (an inline transition object resolves anew each render)\nconst bezierEasings = new Map<string, ReturnType<typeof Easing.bezier> | undefined>()\n\nfunction bezierEasing(easing: string) {\n if (!bezierEasings.has(easing)) {\n // a `linear()` or `steps()` easing has no bezier form, so leave it to\n // reanimated's default rather than applying something that isn't it.\n const bezier = easingToBezier(easing)\n bezierEasings.set(\n easing,\n bezier ? Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) : undefined\n )\n }\n return bezierEasings.get(easing)\n}\n\n/** one resolved entry as reanimated's own animation config */\nfunction entryToReanimated(entry: ResolvedEntry): TransitionConfig {\n if (entry.timing.kind === 'spring') {\n return {\n type: 'spring',\n stiffness: entry.timing.stiffness,\n damping: entry.timing.damping,\n mass: entry.timing.mass,\n delay: entry.delayMs || undefined,\n }\n }\n return {\n type: 'timing',\n duration: entry.timing.durationMs,\n // Easing.bezier returns a worklet, which is what withTiming wants\n easing: bezierEasing(entry.timing.easing),\n delay: entry.delayMs || undefined,\n }\n}\n\n/**\n * Builds animation config from a transition prop.\n * Shared logic used in both initial render and style emitter updates.\n *\n * Everything meaningful happens in `resolveTransition`, which the other three\n * drivers also call. This only translates the result into reanimated's shape.\n */\nfunction buildTransitionConfig(\n transition: any,\n animations: AnimationsConfig,\n animationState: 'enter' | 'exit' | 'default',\n styleKeys: Set<string>\n): TransitionConfigResult {\n const resolved = forAnimationState(\n resolveTransition(transition, { animations: animations as Record<string, unknown> }),\n animationState\n )\n\n const base = resolved.all ? entryToReanimated(resolved.all) : { ...defaultConfig }\n\n const propertyConfigs: Record<string, TransitionConfig> = {}\n for (const key of styleKeys) {\n const entry = getTransitionForKey(resolved, key)\n // a key no entry covers must SNAP, not animate with some default spring.\n // `transition={{ opacity: '150ms' }}` names opacity and nothing else, and\n // that has to mean the same thing here as it does in a stylesheet.\n propertyConfigs[key] = entry ? entryToReanimated(entry) : instantConfig()\n }\n\n return { baseConfig: base, propertyConfigs, resolved }\n}\n\nconst instantConfig = (): TransitionConfig => ({ type: 'timing', duration: 0 })\n\ntype DriverConfig = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n disableAnimation: boolean\n isHydrating: boolean\n}\n\n// configs are flat (numbers plus a shared easing worklet), so a shallow\n// compare is exact\nfunction sameTransitionConfig(a: TransitionConfig, b: TransitionConfig) {\n const aKeys = Object.keys(a)\n if (aKeys.length !== Object.keys(b).length) return false\n for (const key of aKeys) {\n if (a[key as keyof TransitionConfig] !== b[key as keyof TransitionConfig])\n return false\n }\n return true\n}\n\nfunction sameDriverConfig(a: DriverConfig, b: DriverConfig) {\n if (a.disableAnimation !== b.disableAnimation || a.isHydrating !== b.isHydrating) {\n return false\n }\n if (!sameTransitionConfig(a.baseConfig, b.baseConfig)) return false\n const aKeys = Object.keys(a.propertyConfigs)\n if (aKeys.length !== Object.keys(b.propertyConfigs).length) return false\n for (const key of aKeys) {\n const other = b.propertyConfigs[key]\n if (!other || !sameTransitionConfig(a.propertyConfigs[key], other)) return false\n }\n return true\n}\n\n/**\n * Extracts all style keys including transform sub-properties.\n */\nfunction getStyleKeys(style: Record<string, unknown>): Set<string> {\n const keys = new Set(Object.keys(style))\n if (style.transform && Array.isArray(style.transform)) {\n for (const t of style.transform as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n keys.add(Object.keys(t)[0])\n }\n }\n }\n return keys\n}\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Tamagui.\n *\n * This is a native Reanimated implementation without Moti dependency.\n * It provides smooth spring and timing animations with full support for:\n * - Per-property animation configurations\n * - Exit animations with proper completion callbacks\n * - Dynamic theme value resolution\n * - Transform property animations\n * - avoidReRenders optimization for hover/press/focus states\n *\n * @example\n * ```tsx\n * const animations = createAnimations({\n * fast: { duration: 200, bounce: 0.2 },\n * slow: { type: 'timing', duration: 500, easing: 'ease-out' },\n * })\n * ```\n */\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A\n): AnimationDriverWithAnimatedNumbers<A> {\n return {\n needsCustomComponent: true,\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n inputStyle: 'value',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n // binds a descendant that has no font size of its own to the ancestor's\n // animated one. no clock here: the worklet reads the ancestor's shared\n // value, so the font size and a ratio leading land on the same frame.\n useTextMetrics: ({ inheritedText, lineHeight }) => {\n const node =\n inheritedText?.driver === 'reanimated'\n ? (inheritedText.fontSize as SharedValue<number>)\n : null\n // an absolute or `normal` leading still needs the font size bound, it\n // just keeps the leading the caller already resolved.\n const ratio = typeof lineHeight === 'number' ? lineHeight : null\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n if (!node) return {}\n const fontSize = node.value\n return ratio === null ? { fontSize } : { fontSize, lineHeight: fontSize * ratio }\n }, [node, ratio])\n const textChannel = inheritedText ?? null\n // TextInputHost is absent only in the web build, which core never hands a\n // channel, so this is the same condition as `node` twice over.\n if (!node || !TextInputHost) return { style: null, textChannel }\n animatedTextInput ||= Animated.createAnimatedComponent(TextInputHost)\n return {\n // reanimated's animated style is an opaque handle its own hosts read,\n // not a plain object, so it does not fit the contract's Record shape.\n style: animatedStyle as unknown as Record<string, unknown>,\n textChannel,\n Text: isWeb ? AnimatedText : Animated.Text,\n TextInput: animatedTextInput,\n }\n },\n\n // =========================================================================\n // useAnimatedNumber - For imperative animated values\n // =========================================================================\n useAnimatedNumber(initial): UniversalAnimatedNumber<ReanimatedAnimatedNumber> {\n const sharedValue = useSharedValue(initial)\n\n return useMemo(\n () => ({\n getInstance() {\n 'worklet'\n return sharedValue\n },\n\n getValue() {\n 'worklet'\n return sharedValue.value\n },\n\n setValue(next, config = { type: 'spring' }, onFinish) {\n if (config.type === 'direct') {\n sharedValue.value = next\n onFinish?.()\n } else {\n // animations must start on the ui thread in reanimated 4.x -\n // withSpring/withTiming return animation descriptors that only\n // work when assigned to a SharedValue inside the UI runtime\n const cb = onFinish\n ? (finished?: boolean) => {\n 'worklet'\n if (finished !== false) {\n runOnJS(onFinish)()\n }\n }\n : undefined\n\n const animationConfig = cloneTransitionConfig(config)\n\n if (isWeb) {\n // on web there's no UI thread - set directly\n sharedValue.value = applyAnimation(next, animationConfig, cb)\n } else {\n runOnUI((targetValue: number, animationConfig: TransitionConfig) => {\n 'worklet'\n sharedValue.value = applyAnimation(targetValue, animationConfig, cb)\n })(next, animationConfig)\n }\n }\n },\n\n stop() {\n cancelAnimation(sharedValue)\n },\n }),\n [sharedValue]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberReaction - React to animated value changes\n // =========================================================================\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance()\n\n return useAnimatedReaction(\n () => instance.value,\n (next, prev) => {\n if (prev !== next) {\n runOnJS(onValue)(next)\n }\n },\n [onValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberStyle - Create animated styles from values\n // =========================================================================\n useAnimatedNumberStyle(val, getStyle) {\n const instance = val.getInstance()\n\n if (isWeb) {\n // web: must pass explicit deps (no babel plugin)\n return useAnimatedStyle(() => {\n 'worklet'\n return getStyle(instance.value)\n }, [instance, getStyle])\n }\n\n const styleVal = useDerivedValue(() => {\n 'worklet'\n return getStyle(instance.value)\n })\n\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n return styleVal.value\n })\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n return animatedStyle\n },\n\n useAnimatedNumbersStyle(vals, getStyle) {\n const instances = vals.map((v) => v.getInstance())\n\n const animatedStyle = useAnimatedStyle(\n () => {\n 'worklet'\n const currentValues = instances.map((inst) => inst.value)\n return getStyle(...currentValues)\n },\n isWeb ? [getStyle, ...instances] : undefined\n )\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n return animatedStyle\n },\n\n // =========================================================================\n // useAnimations - Main animation hook for components\n // =========================================================================\n useAnimations(animationProps) {\n const {\n props,\n presence,\n style,\n componentState,\n useStyleEmitter,\n themeName,\n stateRef,\n styleState,\n onTransition,\n inheritedText,\n } = animationProps\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n\n // Track if we just finished entering (transition from entering to not entering)\n const wasEnteringRef = useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n // Use effectiveTransition computed by createComponent (single source of truth)\n const effectiveTransition = styleState?.effectiveTransition ?? props.transition\n // core reports a numeric (ratio) leading on nativeTextMetrics; a px\n // length and 'normal' are reported as themselves and stay independent.\n const leadingRatio = getLeadingRatio(styleState?.nativeTextMetrics)\n\n // Use 'enter' if we're mounting OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n const resolvedTransition = forAnimationState(\n resolveTransition(effectiveTransition, {\n animations: animations as Record<string, unknown>,\n }),\n animationState\n )\n\n const disableAnimation = isHydrating || !hasTransition(resolvedTransition)\n\n // this subtree's live font size, mirrored off whatever the host's own\n // fontSize animation paints (see applyAnimation): one clock, not two.\n // descendants read it, and so does this node's own leading whenever that\n // leading is a ratio of the size.\n const fontSizeShared = useSharedValue(\n typeof style.fontSize === 'number' ? style.fontSize : 0\n )\n const publishesFontSize = !disableAnimation && typeof style.fontSize === 'number'\n const ownTextChannel = useMemo(\n () => ({ fontSize: fontSizeShared, driver: 'reanimated' }),\n [fontSizeShared]\n )\n // an inherited font size: core leaves this node's own fontSize out of the\n // style and reports the ratio, so the leading is the ANCESTOR's live font\n // size times this node's ratio, read straight off the ancestor's mirror.\n const inheritedFontSize =\n leadingRatio !== undefined &&\n typeof style.fontSize !== 'number' &&\n inheritedText?.driver === 'reanimated'\n ? (inheritedText.fontSize as SharedValue<number>)\n : null\n // the font size the leading is a ratio OF: an ancestor's when this node\n // has none of its own, otherwise the one this node paints.\n const leadingFontSize = inheritedFontSize ?? fontSizeShared\n // the leading's other factor. the ratio is this node's own property, so it\n // rides the entry that names lineHeight while the font size stays on its\n // own clock, and the product is exact at every frame either way.\n const leadingRatioShared = useSharedValue(leadingRatio ?? 0)\n // the ratio the painted leading was last built from. `undefined` means the\n // leading was a length or `normal`, which has no ratio to move from, so\n // the next ratio starts the product instead of ramping into it.\n const paintedRatioRef = useRef(leadingRatio)\n const applyLeadingRatio = (ratio: number | undefined, config: TransitionConfig) => {\n const previous = paintedRatioRef.current\n if (previous === ratio) return\n paintedRatioRef.current = ratio\n if (ratio === undefined) return\n leadingRatioShared.value =\n previous === undefined || disableAnimation\n ? ratio\n : applyAnimation(ratio, config)\n }\n\n // Theme state for dynamic values - use themeName from props instead of hook\n const isDark = themeName?.startsWith('dark') || false\n\n // Get sendExitComplete callback from presence\n const sendExitComplete = presence?.[1]\n const onTransitionRef = useRef(onTransition)\n onTransitionRef.current = onTransition\n const emit = (\n phase: 'start' | 'end',\n cause: 'enter' | 'exit' | 'update',\n finished?: boolean\n ) => {\n onTransitionRef.current?.(\n phase === 'end' ? { phase, cause, finished } : { phase, cause }\n )\n }\n const enterStartedRef = useRef(false)\n const exitStartedRef = useRef(false)\n const updateStartedRef = useRef(false)\n const isExitingJSRef = useRef(false)\n isExitingJSRef.current = isExiting\n\n // =========================================================================\n // Update cycle state: an in-place style transition while mounted (not\n // enter/exit). drives the onTransition 'update' lifecycle events and the\n // components (e.g. accordion HeightAnimator) that retain content through\n // an in-place transition.\n // =========================================================================\n const updateCycleIdRef = useRef(0)\n const pendingUpdateKeysRef = useRef<Set<string>>(new Set())\n const updateCompletedRef = useRef(false)\n const isCompletingUpdateRef = useSharedValue(false)\n const updateCycleIdShared = useSharedValue(0)\n // an interrupted key still resolves its participation in this cycle. it\n // used to be dropped on the floor, which left the key in the pending set\n // forever and hung the update cycle: no 'end' event, and any component\n // waiting on one (accordion's HeightAnimator) stayed latched.\n const updateInterruptedRef = useRef(false)\n const markUpdateKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n if (cycleId !== updateCycleIdRef.current) return\n if (updateCompletedRef.current) return\n if (!finished) updateInterruptedRef.current = true\n pendingUpdateKeysRef.current.delete(key)\n if (pendingUpdateKeysRef.current.size === 0) {\n updateCompletedRef.current = true\n isCompletingUpdateRef.value = false\n if (updateStartedRef.current) {\n updateStartedRef.current = false\n emit('end', 'update', !updateInterruptedRef.current)\n }\n }\n }\n )\n\n // =========================================================================\n // Exit cycle state for deterministic per-property completion tracking\n // =========================================================================\n const exitCycleIdRef = useRef(0)\n const pendingExitKeysRef = useRef<Set<string>>(new Set())\n const exitCompletedRef = useRef(false)\n const wasExitingRef = useRef(false)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // stable callback to mark a property as done (called from worklet via runOnJS)\n const markExitKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n // ignore callbacks from stale cycles\n if (cycleId !== exitCycleIdRef.current) return\n // ignore if already completed\n if (exitCompletedRef.current) return\n // count both finished and canceled animations as \"done\" during exit\n // (element is leaving anyway, canceled animations shouldn't block completion)\n\n pendingExitKeysRef.current.delete(key)\n\n // check if all exit animations are done\n if (pendingExitKeysRef.current.size === 0) {\n exitCompletedRef.current = true\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n // exit 'end' fires immediately before presence safeToRemove\n emit('end', 'exit', true)\n }\n sendExitComplete?.()\n }\n }\n )\n\n // =========================================================================\n // Enter cycle state for the onTransition enter 'end' event\n // =========================================================================\n const enterCycleIdRef = useRef(0)\n const pendingEnterKeysRef = useRef<Set<string>>(new Set())\n const enterCompletedRef = useRef(false)\n\n const markEnterKeyDone = useEvent((key: string, cycleId: number) => {\n if (cycleId !== enterCycleIdRef.current) return\n if (enterCompletedRef.current) return\n if (isExitingJSRef.current) return\n\n pendingEnterKeysRef.current.delete(key)\n\n if (pendingEnterKeysRef.current.size === 0) {\n enterCompletedRef.current = true\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n }\n })\n\n // SharedValue to pass exit state into worklet\n const isExitingRef = useSharedValue(isExiting)\n const exitCycleIdShared = useSharedValue(exitCycleIdRef.current)\n const isCompletingEnterRef = useSharedValue(false)\n const enterCycleIdShared = useSharedValue(enterCycleIdRef.current)\n\n // start new exit cycle only on transition INTO exiting (not every render while exiting)\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n pendingExitKeysRef.current.clear()\n enterCycleIdRef.current++\n pendingEnterKeysRef.current.clear()\n updateCycleIdRef.current++\n pendingUpdateKeysRef.current.clear()\n updateCompletedRef.current = true\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n pendingExitKeysRef.current.clear()\n }\n\n // update shared values (using effect to avoid writing during render)\n useIsomorphicLayoutEffect(() => {\n isExitingRef.value = isExiting\n exitCycleIdShared.value = exitCycleIdRef.current\n isCompletingEnterRef.value =\n !isExiting && justFinishedEntering && Boolean(onTransitionRef.current)\n enterCycleIdShared.value = enterCycleIdRef.current\n if (justStartedExiting) {\n isCompletingUpdateRef.value = false\n updateCycleIdShared.value = updateCycleIdRef.current\n }\n }, [\n isExiting,\n justFinishedEntering,\n exitCycleIdRef.current,\n enterCycleIdRef.current,\n justStartedExiting,\n ])\n\n // exit interrupted by a re-enter: report the exit as finished:false\n useIsomorphicLayoutEffect(() => {\n if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {\n exitStartedRef.current = false\n emit('end', 'exit', false)\n }\n }, [justStoppedExiting])\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\n })\n\n // =========================================================================\n // avoidRerenders: SharedValues for style updates without re-renders\n // =========================================================================\n const committedRenderKeysRef = useRef(new Set<string>())\n // full painted style of the last committed source (render or emitter):\n // statics + animated targets, transform sub-keys flattened as `transform:key`.\n // seeds read from it.\n const lastPaintedRef = useRef<Record<string, unknown>>({})\n // per-key first-animated values React keeps painting under the mapper —\n // see the styles memo below\n const carryRef = useRef<Record<string, unknown>>({})\n\n // commits during exit must keep the predecessor from before exit began,\n // even if they publish again before the UI mapper consumes the first one.\n const exitSeedRef = useRef<Record<string, unknown> | null>(null)\n const paintedPredecessor = isExiting\n ? (exitSeedRef.current ?? lastPaintedRef.current)\n : lastPaintedRef.current\n useIsomorphicLayoutEffect(() => {\n exitSeedRef.current = isExiting ? paintedPredecessor : null\n }, [isExiting, paintedPredecessor])\n\n // Separate styles into animated and static\n const { animatedStyles, staticStyles, nextCarry } = useMemo(() => {\n const { animated, statics } = splitAnimationStyles(\n style as Record<string, unknown>,\n isDark,\n disableAnimation\n )\n\n // retain the predecessor's composition when exit only targets some transforms.\n if (isExiting && animated.transform) {\n const remaining = getAnimatedTransforms(animated.transform).map(\n cloneStyleRecord\n )\n const previous = getAnimatedTransforms(paintedPredecessor.transform)\n if (previous.length) {\n animated.transform = previous\n .map((entry) => {\n const key = Object.keys(entry)[0]\n const index = remaining.findIndex((target) => key in target)\n return index < 0 ? cloneStyleRecord(entry) : remaining.splice(index, 1)[0]\n })\n .concat(remaining)\n }\n }\n\n // every animated key keeps its FIRST animated value in React's style for\n // as long as it stays animated. the value never changes across renders,\n // so React's per-key style diff never touches the key again and the\n // mapper's inline writes survive every commit — reanimated's Fabric\n // commit hook provides this guarantee on native, web has no equivalent.\n // during enter the carry is the enter-clause value (the mapper animates over it);\n // a key appearing post-mount (a just-measured height) paints its value in\n // the same commit the mapper first sees it, a frame before the mapper's\n // first write.\n // derive from committed state. mutating carryRef during render leaks\n // abandoned concurrent renders into later commits.\n const nextCarry = cloneStyleRecord(carryRef.current)\n for (const key in nextCarry) {\n if (!(key in animated)) delete nextCarry[key]\n }\n for (const key in animated) {\n if (!(key in nextCarry)) {\n const target = animated[key]\n if (isExiting && key === 'transform') {\n nextCarry[key] = getAnimatedTransforms(target).map((transform) => {\n const initial = cloneStyleRecord(transform)\n for (const part in initial) {\n const value = initial[part]\n if (typeof value === 'number' || typeof value === 'string') {\n initial[part] =\n paintedPredecessor[`transform:${part}`] ??\n getImplicitDefault(part, value)\n }\n }\n return initial\n })\n } else {\n nextCarry[key] = cloneAnimationValue(\n isExiting && (typeof target === 'number' || typeof target === 'string')\n ? (paintedPredecessor[key] ?? getImplicitDefault(key, target))\n : target\n )\n }\n }\n statics[key] = nextCarry[key]\n }\n\n return { animatedStyles: animated, staticStyles: statics, nextCarry }\n }, [\n disableAnimation,\n style,\n isDark,\n isExiting,\n isExiting ? paintedPredecessor : null,\n ])\n\n const derivesLeading = derivesLeadingProduct(\n animatedStyles,\n leadingRatio,\n !!inheritedFontSize,\n resolvedTransition\n )\n\n const renderSnapshot = useMemo(\n () =>\n buildSnapshot(\n animatedStyles,\n staticStyles,\n getAnimatedTransforms(animatedStyles.transform),\n committedRenderKeysRef.current,\n paintedPredecessor,\n derivesLeading\n ),\n [\n animatedStyles,\n staticStyles,\n isExiting ? paintedPredecessor : null,\n derivesLeading,\n ]\n )\n const renderSnapshotRef = useSharedValue<AnimationSnapshot>({\n animated: {},\n statics: {},\n transforms: [],\n gatedKeys: {},\n derivesLeading: false,\n seeds: {},\n removedKeys: {},\n removeTransform: false,\n clearValue: isWeb ? '' : null,\n })\n const emitterSnapshotRef = useSharedValue<AnimationSnapshot | null>(null)\n const emitterKeysRef = useRef<Set<string> | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n carryRef.current = nextCarry\n committedRenderKeysRef.current = renderSnapshot.keys\n lastPaintedRef.current = renderSnapshot.painted\n }, [nextCarry, renderSnapshot])\n\n // reconcile the emitter latch with real re-renders.\n //\n // the avoidReRenders fast path lets a pure pseudo change (hover/press/focus) push\n // styles straight to the worklet via useStyleEmitter with no React commit — it sets\n // emitterSnapshotRef. the worklet then treats `emitterSnapshotRef !== null` as a\n // permanent latch: once the emitter has fired even once, it reads its last-emitted\n // snapshot and IGNORES `animatedStyles` from every subsequent re-render. so a base\n // style that changes through React (e.g. a row's `backgroundColor` flipping with an\n // external selection store, not a hover) never reaches the screen — it stays stuck on\n // the stale emitted value until the next hover re-fires the emitter. (the \"active row\n // highlight gets stuck until you hover it again\" report.)\n //\n // a real re-render is the freshest source of truth for the base style, so on render we\n // drop the emitter latch and let the worklet fall back to `animatedStyles`. the one\n // exception: while a self pseudo (hover/press/focus) is active the emitted style is a\n // transient override that isn't in this render, so keep it latched — otherwise an\n // unrelated re-render while hovering would snap back to the base. `pseudoActiveRef`\n // tracks that flag straight off the emitter (which fires on every pseudo transition), so\n // the un-hover/un-press emission flips it false and the next render reclaims the style.\n // no value comparison needed; runtime media stays correct because a render re-reads live\n // media into `animatedStyles`.\n // exit overrides the pseudo exception: the exit render's style IS the\n // authoritative target, and an element that exits while hovered/focused (every\n // dialog closed by clicking a button inside it) would otherwise keep the stale\n // emitted base latched — the worklet then \"animates\" to values already on\n // screen, every exit-key callback completes in a frame, and AnimatePresence\n // unmounts immediately (the dialog-exit-snap). mirrored into a plain ref so the\n // emitter callback (which runs outside render) can read it.\n const pseudoActiveRef = useRef(false)\n // the committed render is the worklet's source of truth whenever the emitter is not\n // latched, so every render must publish its snapshot — not just the renders that drop\n // a latch. animated keys live only in this snapshot after mount (staticStyles carries\n // them during mount only), so a render that never publishes leaves the worklet reading\n // an empty snapshot and the animated properties never reach the screen at all.\n // The first emitter snapshot can already contain the browser's real media-query state\n // while the first render still contains mediaQueryDefaultActive for hydration. Publish\n // that render behind the latch, but do not let it replace the fresher runtime snapshot.\n const publishedSnapshotRef = useRef<object | null>(null)\n useIsomorphicLayoutEffect(() => {\n const renderChanged = publishedSnapshotRef.current !== renderSnapshot.value\n const droppingLatch =\n (isExiting ||\n (!pseudoActiveRef.current &&\n publishedSnapshotRef.current !== null &&\n renderChanged)) &&\n emitterSnapshotRef.value !== null\n // when the latch drops, keys the emitter owned that this render no longer has must\n // be cleared too, otherwise reanimated keeps painting the stale emitted value\n const emitterKeys = droppingLatch ? emitterKeysRef.current : null\n\n if (droppingLatch || renderChanged) {\n const removedKeys = emitterKeys\n ? {\n ...renderSnapshot.value.removedKeys,\n ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys),\n }\n : renderSnapshot.value.removedKeys\n renderSnapshotRef.value = {\n ...renderSnapshot.value,\n removedKeys,\n removeTransform: Object.keys(removedKeys).some((key) =>\n key.startsWith('transform:')\n ),\n }\n publishedSnapshotRef.current = renderSnapshot.value\n }\n\n if (droppingLatch) {\n emitterSnapshotRef.value = null\n emitterKeysRef.current = null\n }\n })\n\n // Build animation config with per-property overrides using normalized transition\n const { baseConfig, propertyConfigs } = useMemo(() => {\n if (isHydrating) {\n return {\n baseConfig: { type: 'timing' as const, duration: 0 },\n propertyConfigs: {} as Record<string, TransitionConfig>,\n }\n }\n\n // use effectiveTransition (createComponent's single source of truth) so a\n // platform-pseudo component with no declared transition resolves its BASE\n // config to instant ('0ms') too — not just its pseudo flips. otherwise base\n // style changes on the driver path animate with the default spring and a\n // hover-out / base color change visibly lingers (looks \"stuck\").\n return buildTransitionConfig(\n effectiveTransition,\n animations,\n animationState,\n getStyleKeys(animatedStyles)\n )\n }, [isHydrating, effectiveTransition, animatedStyles, animationState])\n\n // Store config in SharedValue for worklet access (concurrent-safe)\n // Using useEffect to avoid writing to shared value during render\n const renderConfig: DriverConfig = {\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n }\n const configRef = useSharedValue(renderConfig)\n // the last config written from JS. every write serializes it to the UI\n // runtime and wakes the style mapper, and a render rebuilds the config\n // whenever animatedStyles changes identity, so writes that change\n // nothing are skipped (the mount write repeats the seed above)\n const writtenConfigRef = useRef(renderConfig)\n const writeConfig = (next: DriverConfig) => {\n if (sameDriverConfig(writtenConfigRef.current, next)) return\n writtenConfigRef.current = next\n configRef.value = next\n }\n\n useIsomorphicLayoutEffect(() => {\n writeConfig(renderConfig)\n }, [baseConfig, propertyConfigs, disableAnimation, isHydrating])\n\n // only the style the mapper is actually reading may move the ratio: while\n // an emitted pseudo style is latched it owns the leading, and the emitter\n // has already written the ratio that goes with it.\n useIsomorphicLayoutEffect(() => {\n if (emitterSnapshotRef.value !== null) return\n applyLeadingRatio(leadingRatio, propertyConfigs.lineHeight ?? baseConfig)\n })\n\n // =========================================================================\n // avoidRerenders: register style emitter callback\n // when hover/press/etc state changes, this is called instead of re-rendering\n // =========================================================================\n // the fourth argument is the emitted style's own nativeTextMetrics: the\n // emitter re-runs getSplitStyles outside render, so the ratio that\n // describes the style it hands over is never the ratio the last render\n // resolved (a pseudo can replace the leading with an absolute length).\n const onEmittedStyle = (\n nextStyle: Record<string, unknown>,\n effectiveTransition: TransitionProp | null | undefined,\n pseudoActive?: boolean,\n nextMetrics?: NativeTextMetrics\n ) => {\n // while exiting, the exit render owns the style — a pseudo flip mid-exit\n // (hover-out as the element fades under the cursor) must not re-latch the\n // base style over the in-flight exit targets.\n if (isExitingJSRef.current) return\n // track whether a self pseudo is active so the render-time layout effect knows whether\n // this emitter snapshot is a transient override to keep latched or a base it can drop.\n pseudoActiveRef.current = pseudoActive === true\n\n // effectiveTransition is computed in createComponent based on entering/exiting pseudo states\n // rebuild config whenever transition changes (entering OR exiting pseudo states)\n const transitionToUse = effectiveTransition ?? props.transition\n const emittedLeadingRatio = getLeadingRatio(nextMetrics)\n const {\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n resolved: emittedResolved,\n } = buildTransitionConfig(\n transitionToUse,\n animations,\n animationState,\n getStyleKeys(nextStyle)\n )\n\n const written = writtenConfigRef.current\n writeConfig({\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n disableAnimation: written.disableAnimation,\n isHydrating: written.isHydrating,\n })\n\n const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current\n const { animated, statics } = splitAnimationStyles(\n nextStyle,\n isDark,\n written.disableAnimation\n )\n // the emitted style resolves its own leading: a pseudo can replace a\n // ratio with a length, which unbinds the product for as long as it holds\n const emittedDerivesLeading = derivesLeadingProduct(\n animated,\n emittedLeadingRatio,\n !!inheritedFontSize,\n emittedResolved\n )\n applyLeadingRatio(emittedLeadingRatio, newPropertyConfigs.lineHeight ?? newBase)\n const snapshot = buildSnapshot(\n animated,\n statics,\n getAnimatedTransforms(animated.transform),\n previousKeys,\n lastPaintedRef.current,\n emittedDerivesLeading\n )\n emitterSnapshotRef.value = snapshot.value\n emitterKeysRef.current = snapshot.keys\n lastPaintedRef.current = snapshot.painted\n\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useStyleEmitter update', {\n animated,\n statics,\n transforms: snapshot.value.transforms,\n })\n }\n }\n useStyleEmitter?.(onEmittedStyle)\n\n // Compute and register exit keys synchronously during render to avoid race conditions\n // This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set\n const exitKeysRegistered = useRef(false)\n const enterKeysRegistered = useRef(false)\n\n if (!isExiting && justFinishedEntering && onTransition) {\n const enterKeys = getGatedKeys(renderSnapshot.value)\n enterCycleIdRef.current++\n enterCompletedRef.current = false\n pendingEnterKeysRef.current = new Set(enterKeys)\n enterKeysRegistered.current = enterKeys.length > 0\n }\n\n if (justStartedExiting && sendExitComplete) {\n const exitKeys = getGatedKeys(renderSnapshot.value)\n pendingExitKeysRef.current = new Set(exitKeys)\n exitKeysRegistered.current = exitKeys.length > 0\n }\n\n // update cycle: a genuine in-place style change while mounted (not\n // entering or exiting). register the exact keys that can animate before\n // publishing the cycle to the worklet; shared values are only written\n // from the layout effect, never from the mapper that reads them.\n const previousUpdateStyleRef = useRef<string | null>(null)\n const updateStyle = onTransition ? JSON.stringify(style) : null\n const previousUpdateStyle = previousUpdateStyleRef.current\n const shouldRegisterUpdate =\n updateStyle !== null &&\n previousUpdateStyle !== null &&\n previousUpdateStyle !== updateStyle &&\n !isExiting &&\n !isEntering &&\n !justFinishedEntering\n const updateKeys = shouldRegisterUpdate ? getGatedKeys(renderSnapshot.value) : []\n\n useIsomorphicLayoutEffect(() => {\n if (updateStyle === null) {\n previousUpdateStyleRef.current = null\n isCompletingUpdateRef.value = false\n return\n }\n\n // only a committed render may advance the authoritative cycle. the\n // captured previous value prevents StrictMode's repeated effect from\n // registering the same transition twice.\n const previousStyleStillCurrent =\n previousUpdateStyleRef.current === previousUpdateStyle\n previousUpdateStyleRef.current = updateStyle\n if (!shouldRegisterUpdate || !previousStyleStillCurrent) return\n\n const cycleId = ++updateCycleIdRef.current\n pendingUpdateKeysRef.current = new Set(updateKeys)\n updateInterruptedRef.current = false\n updateCompletedRef.current = updateKeys.length === 0\n isCompletingUpdateRef.value = updateKeys.length > 0\n updateCycleIdShared.value = cycleId\n\n // a superseded in-flight update reports finished:false before the new\n // cycle starts; a zero-key cycle reports an immediate start/end pair\n if (updateStartedRef.current) {\n emit('end', 'update', false)\n }\n updateStartedRef.current = true\n emit('start', 'update')\n if (updateKeys.length === 0) {\n updateStartedRef.current = false\n emit('end', 'update', true)\n }\n }, [updateStyle, shouldRegisterUpdate])\n\n // handle zero-animation case in effect (after render commit)\n React.useEffect(() => {\n if (!justStartedExiting || !sendExitComplete) return\n\n // enter or update still in flight when exit begins is reported as finished:false\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', false)\n }\n if (updateStartedRef.current) {\n updateStartedRef.current = false\n emit('end', 'update', false)\n }\n\n // emit exit start once per exit cycle\n if (onTransitionRef.current && !exitStartedRef.current) {\n exitStartedRef.current = true\n emit('start', 'exit')\n }\n\n // if no keys were registered, complete immediately\n if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {\n if (!exitCompletedRef.current) {\n exitCompletedRef.current = true\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n emit('end', 'exit', true)\n }\n sendExitComplete()\n }\n }\n }, [justStartedExiting, sendExitComplete])\n\n React.useEffect(() => {\n if (isExiting || !justFinishedEntering || !onTransition) return\n\n // emit enter start once per enter cycle\n if (!enterStartedRef.current) {\n enterStartedRef.current = true\n emit('start', 'enter')\n }\n\n if (!enterKeysRegistered.current && pendingEnterKeysRef.current.size === 0) {\n if (!enterCompletedRef.current) {\n enterCompletedRef.current = true\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n }\n }\n }, [isExiting, justFinishedEntering, onTransition])\n\n // the worklet's own record of which animated keys it has emitted (mirrors\n // reanimated's per-view style history, which resets whenever a key leaves\n // the output). a key not in here is fresh to the mapper no matter what\n // React has committed — several renders can coalesce into one mapper run.\n // native updates use modify with the same object and no forced update, so\n // the mapper doesn't react to its own history write.\n const mapperStateRef = useSharedValue<MapperState>({\n emitted: {},\n ownedStatics: {},\n })\n // Create animated style\n const animatedStyle = useAnimatedStyle(\n () => {\n 'worklet'\n\n const mapperState = mapperStateRef.value\n const config = configRef.value\n if (config.disableAnimation || config.isHydrating) {\n // the empty return wipes reanimated's per-key history, so ours\n // resets with it\n updateMapperState(mapperStateRef, {}, {})\n return {}\n }\n\n const previouslyEmitted = mapperState.emitted\n const emitted: Record<string, boolean> = {}\n const previouslyOwned = mapperState.ownedStatics\n const ownedStatics: Record<string, boolean> = {}\n\n const result: Record<string, any> = {}\n\n // Check if we have avoidRerenders updates from useStyleEmitter\n const emitterSnapshot = emitterSnapshotRef.value\n const snapshot = emitterSnapshot ?? renderSnapshotRef.value\n\n // Use emitter values if available, otherwise use the committed render snapshot.\n // statics fall back to the render's staticStyles for keys the mapper\n // owns (see MapperState): once an emitter snapshot has applied a\n // static key, reanimated never unsets it, so after the latch drops a\n // stale emitted value (e.g. a border color that could not animate)\n // would keep painting over the fresh style prop forever\n const animatedValues = snapshot.animated\n const staticValues = snapshot.statics\n\n // read lifecycle state from shared values\n const currentlyExiting = isExitingRef.value\n const currentCycleId = exitCycleIdShared.value\n const currentlyCompletingEnter = isCompletingEnterRef.value\n const currentEnterCycleId = enterCycleIdShared.value\n const currentlyCompletingUpdate = isCompletingUpdateRef.value\n const currentUpdateCycleId = updateCycleIdShared.value\n\n // Include static values from emitter (for hover/press style changes).\n // a render snapshot only refreshes keys an emitter already wrote.\n for (const key in staticValues) {\n if (!emitterSnapshot && !previouslyOwned[key]) continue\n ownedStatics[key] = true\n result[key] = staticValues[key]\n }\n for (const key in snapshot.removedKeys) {\n if (!key.startsWith('transform:') && !(key in staticValues)) {\n result[key] = snapshot.clearValue\n }\n }\n\n // Animate regular properties\n for (const key in animatedValues) {\n if (key === 'transform') continue\n\n const targetValue = animatedValues[key]\n emitted[key] = true\n // taken after this loop, so the font size it multiplies is the one\n // this frame paints\n if (key === 'lineHeight' && snapshot.derivesLeading) continue\n if (typeof targetValue !== 'number' && typeof targetValue !== 'string') {\n result[key] = targetValue\n continue\n }\n result[key] = animateSnapshotValue(\n key,\n key,\n targetValue,\n config.propertyConfigs[key] ?? config.baseConfig,\n !!previouslyEmitted[key],\n snapshot.seeds[key],\n !!snapshot.gatedKeys[key],\n currentlyExiting,\n currentCycleId,\n currentlyCompletingEnter,\n currentEnterCycleId,\n currentlyCompletingUpdate,\n currentUpdateCycleId,\n markExitKeyDone,\n markEnterKeyDone,\n markUpdateKeyDone,\n !!COLOR_STYLE_KEYS[key],\n key === 'fontSize' ? fontSizeShared : undefined\n )\n }\n\n // the live font size this leading is a ratio of, times the ratio: a\n // product, so both factors are the ones on screen this frame\n if (snapshot.derivesLeading) {\n emitted.lineHeight = true\n result.lineHeight = leadingFontSize.value * leadingRatioShared.value\n }\n\n // Handle transforms\n const transforms = snapshot.transforms\n\n // Animate transform properties with validation\n if (transforms.length > 0 || snapshot.removeTransform) {\n const validTransforms: Record<string, unknown>[] = []\n\n for (const t of transforms) {\n if (!t) continue\n const keys = Object.keys(t)\n if (keys.length === 0) continue\n const transformKey = keys[0]\n const value = t[transformKey]\n if (typeof value === 'number' || typeof value === 'string') {\n const propConfig =\n config.propertyConfigs[transformKey] ?? config.baseConfig\n\n const subKey = `transform:${transformKey}`\n emitted[subKey] = true\n validTransforms.push({\n [transformKey]: animateSnapshotValue(\n subKey,\n transformKey,\n value,\n propConfig,\n !!previouslyEmitted[subKey],\n snapshot.seeds[subKey],\n !!snapshot.gatedKeys[subKey],\n currentlyExiting,\n currentCycleId,\n currentlyCompletingEnter,\n currentEnterCycleId,\n currentlyCompletingUpdate,\n currentUpdateCycleId,\n markExitKeyDone,\n markEnterKeyDone,\n markUpdateKeyDone\n ),\n })\n } else {\n validTransforms.push(t)\n }\n }\n\n if (validTransforms.length > 0 || !('transform' in staticValues)) {\n result.transform = validTransforms\n }\n }\n\n updateMapperState(mapperStateRef, emitted, ownedStatics)\n\n return result\n },\n isWeb\n ? [\n // pass SharedValues so the stable mapper watches them on web (no babel plugin)\n renderSnapshotRef,\n emitterSnapshotRef,\n configRef,\n isExitingRef,\n exitCycleIdShared,\n markExitKeyDone,\n isCompletingEnterRef,\n enterCycleIdShared,\n markEnterKeyDone,\n isCompletingUpdateRef,\n updateCycleIdShared,\n markUpdateKeyDone,\n fontSizeShared,\n leadingRatioShared,\n inheritedFontSize,\n ]\n : undefined\n )\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n // Debug logging\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useAnimations', {\n resolvedTransition,\n componentState,\n isExiting,\n animatedStyles,\n staticStyles,\n baseConfig,\n propertyConfigs,\n })\n }\n\n // staticStyles carries animated keys only while mounting or on a key's\n // first post-mount appearance; after that the mapper owns them and the\n // commit bridge above keeps React commits from wiping its inline writes.\n return {\n style: [staticStyles, animatedStyle],\n // what descendants inherit: this node's mirror when it owns a font size\n // that moves, nothing when it owns one that does not (react-native\n // inherits that statically), otherwise what it inherited itself\n textChannel: publishesFontSize\n ? ownTextChannel\n : typeof style.fontSize === 'number'\n ? null\n : (inheritedText ?? null),\n }\n },\n }\n}\n"
|
|
9
|
+
"import {\n easingToBezier,\n forAnimationState,\n getTransitionForKey,\n hasTransition,\n resolveTransition,\n type ResolvedEntry,\n type ResolvedTransition,\n type AnimationsConfig,\n} from '@tamagui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useEvent,\n useIsomorphicLayoutEffect,\n View,\n type AnimationDriverWithAnimatedNumbers,\n type NativeTextMetrics,\n type TransitionProp,\n type UniversalAnimatedNumber,\n createRefComponent,\n} from '@tamagui/core'\nimport { useThemeWithState } from '@tamagui/web/internal-runtime'\nimport { TextInputHost } from './textInputHost'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport normalizeColor from '@react-native/normalize-colors'\nimport React, { useMemo, useRef } from 'react'\nimport type { SharedValue } from 'react-native-reanimated'\nimport Animated_, {\n cancelAnimation,\n Easing,\n runOnJS,\n runOnUI,\n useAnimatedReaction,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withDelay,\n withSpring,\n withTiming,\n type WithSpringConfig,\n type WithTimingConfig,\n} from 'react-native-reanimated'\n\n// sizing keywords and calc() have no numeric endpoint and cannot gate exits\nconst NON_ANIMATABLE_VALUE =\n /^(auto|min-content|max-content|fit-content|stretch|inherit|initial|unset)\\b|^calc\\(/\n\n// =============================================================================\n// ESM/CJS compatibility\n// =============================================================================\n\n/**\n * Handle ESM/CJS module interop for react-native-reanimated\n * @see https://github.com/evanw/esbuild/issues/2480#issuecomment-1833104754\n */\nconst getDefaultExport = <T,>(module: T | { default: T }): T => {\n const mod = module as any\n if (mod.__esModule || mod[Symbol.toStringTag] === 'Module') {\n return mod.default || mod\n }\n return mod\n}\n\nconst Animated = getDefaultExport(Animated_)\n\nconst silenceAnimatedComponentDevCheck = (style: unknown) => {\n if (\n process.env.NODE_ENV !== 'development' ||\n isWeb ||\n !style ||\n typeof style !== 'object'\n ) {\n return\n }\n\n // react fabric's dev performance logger diffs component props and reads the\n // reanimated guard getter even when the style is on an animated component.\n Object.defineProperty(style, '_requiresAnimatedComponent', {\n configurable: true,\n enumerable: false,\n value: true,\n })\n}\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype ReanimatedAnimatedNumber = SharedValue<number>\n\ntype AnimationSnapshot = {\n animated: Record<string, unknown>\n statics: Record<string, unknown>\n transforms: Array<Record<string, unknown>>\n gatedKeys: Record<string, boolean>\n /**\n * the leading is a ratio of a font size that can move under it, so the mapper\n * multiplies the two live factors instead of animating a lineHeight of its\n * own. false for an absolute length, for `normal`, and for a font size that\n * is already at rest.\n */\n derivesLeading: boolean\n /**\n * for each animated key (transform sub-keys as `transform:key`), the value the\n * previous committed render painted for it, when one exists and is animatable.\n * the worklet animates a mapper-fresh key FROM its seed instead of snapping —\n * this is how an enter-clause value painted statically during mount becomes the\n * start point once the key turns animated. keys with no seed paint their target\n * directly (a key appearing from nothing has nothing to animate from).\n */\n seeds: Record<string, unknown>\n removedKeys: Record<string, boolean>\n removeTransform: boolean\n clearValue: '' | null\n}\n\n/** Spring animation configuration */\ntype SpringConfig = {\n type?: 'spring'\n delay?: number\n} & Partial<WithSpringConfig>\n\n/** Timing animation configuration */\ntype TimingConfig = {\n type: 'timing'\n delay?: number\n} & Partial<WithTimingConfig>\n\n/** Combined animation configuration type */\nexport type TransitionConfig = SpringConfig | TimingConfig\n\n/** Options for createAnimations (reserved for future use) */\nexport type CreateAnimationsOptions = {}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Resolve dynamic theme values (e.g., `{dynamic: {dark: \"value\", light: \"value\"}}`)\n */\nconst resolveDynamicValue = (value: unknown, isDark: boolean): unknown => {\n if (\n value !== null &&\n typeof value === 'object' &&\n 'dynamic' in value &&\n typeof (value as any).dynamic === 'object'\n ) {\n const dynamic = (value as any).dynamic\n return isDark ? dynamic.dark : dynamic.light\n }\n return value\n}\n\n/** Animation completion callback type */\ntype AnimationCallback = (finished?: boolean) => void\n\ntype MapperState = {\n emitted: Record<string, boolean>\n // static keys a style emitter has written through the mapper. nothing else\n // may emit statics: reanimated snapshots this worklet's first-render output\n // per style handle and re-applies that snapshot under every later style\n // prop, so a static emitted on mount (a disabled mount's pointerEvents:\n // 'none', its opacity) would outlive the render that removed it. render\n // statics reach the view through the staticStyles element instead.\n ownedStatics: Record<string, boolean>\n}\n\nconst updateMapperState = isWeb\n ? (\n state: SharedValue<MapperState>,\n emitted: Record<string, boolean>,\n ownedStatics: Record<string, boolean>\n ) => {\n state.value.emitted = emitted\n state.value.ownedStatics = ownedStatics\n }\n : (\n state: SharedValue<MapperState>,\n emitted: Record<string, boolean>,\n ownedStatics: Record<string, boolean>\n ) => {\n 'worklet'\n if (!globalThis._WORKLET) return\n state.modify((current) => {\n current.emitted = emitted\n current.ownedStatics = ownedStatics\n return current\n }, false)\n }\n\nconst cloneAnimationValue = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(cloneAnimationValue)\n }\n\n if (value && typeof value === 'object') {\n const next: Record<string, unknown> = {}\n for (const key in value as Record<string, unknown>) {\n next[key] = cloneAnimationValue((value as Record<string, unknown>)[key])\n }\n return next\n }\n\n return value\n}\n\nconst cloneStyleRecord = (style: Record<string, unknown>): Record<string, unknown> => {\n const next: Record<string, unknown> = {}\n for (const key in style) {\n next[key] = cloneAnimationValue(style[key])\n }\n return next\n}\n\nconst cloneTransitionConfig = (config: TransitionConfig): TransitionConfig => {\n return cloneAnimationValue(config) as TransitionConfig\n}\n\nconst getAnimatedTransforms = (value: unknown): Array<Record<string, unknown>> => {\n if (!Array.isArray(value)) return []\n return value.filter(\n (item): item is Record<string, unknown> =>\n item !== null && typeof item === 'object' && !Array.isArray(item)\n )\n}\n\nconst getRemovedAnimatedKeys = (\n current: Set<string>,\n previous: Set<string>\n): Record<string, boolean> => {\n const removedKeys: Record<string, boolean> = {}\n for (const key of previous) {\n if (!current.has(key)) removedKeys[key] = true\n }\n return removedKeys\n}\n\n// implicit start values for style keys that were never painted (e.g. a key\n// introduced by an exit clause with no base value). most numerics default to 0.\nconst getImplicitDefault = (\n key: string,\n targetValue: number | string\n): number | string => {\n 'worklet'\n const value =\n key === 'opacity' || key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0\n if (typeof targetValue !== 'string') return value\n\n // reanimated derives a string animation's suffix from its start value. keep\n // units on implicit starts so fresh `50%` and `90deg` keys interpolate.\n let suffixIndex = 0\n while (suffixIndex < targetValue.length) {\n const character = targetValue[suffixIndex]\n const code = character.charCodeAt(0)\n if (\n (code >= 48 && code <= 57) ||\n character === '.' ||\n character === '-' ||\n character === '+'\n ) {\n suffixIndex++\n } else {\n break\n }\n }\n return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value\n}\n\n// A numeric authored lineHeight is a RATIO of the resolved font size, not a\n// length. Core finalizes the destination to fontSize * ratio and reports the\n// ratio back on `nativeTextMetrics`, so the painted leading has to be\n// fontSize * ratio on EVERY frame, not only at the destination. An absolute px\n// leading, and any raw react-native style, keeps its own value and its own\n// timeline.\nconst getLeadingRatio = (metrics: { lineHeight?: unknown } | undefined | null) => {\n const ratio = metrics?.lineHeight\n return typeof ratio === 'number' && Number.isFinite(ratio) && ratio >= 0\n ? ratio\n : undefined\n}\n\n// The mapper cannot multiply two reanimated DESCRIPTORS, but it can multiply\n// two live numbers: the font size this node paints is mirrored into a shared\n// value frame by frame (see applyAnimation) and the ratio rides a shared value\n// of its own, so the leading is their product, recomputed on every frame either\n// factor moves. That is what makes a font size and a ratio changing together\n// paint fs(t) * r(t) instead of a ramp to fs_end * r_end.\n//\n// A transition covering neither key leaves both to land in the same commit,\n// already coherent, so there is nothing to derive. An inherited font size runs\n// on the ancestor's clock and never has an entry here, so it always derives.\nconst derivesLeadingProduct = (\n style: Record<string, unknown>,\n ratio: number | undefined,\n inheritsFontSize: boolean,\n resolved: ResolvedTransition\n) =>\n ratio !== undefined &&\n (inheritsFontSize ||\n (typeof style.lineHeight === 'number' &&\n typeof style.fontSize === 'number' &&\n (!!getTransitionForKey(resolved, 'fontSize') ||\n !!getTransitionForKey(resolved, 'lineHeight'))))\n\nconst COLOR_STYLE_KEYS: Record<string, boolean> = {\n backgroundColor: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n color: true,\n}\n\n// reanimated can pass either its rgba output or a processed numeric color back\n// as animation history. snapshot targets are normalized on the JS side below;\n// this worklet conversion exists only to validate that in-flight history.\nconst toReanimatedColor = (value: unknown): unknown => {\n 'worklet'\n if (typeof value === 'number') {\n const color = value >>> 0\n const alpha = ((color >>> 24) & 255) / 255\n const red = (color >>> 16) & 255\n const green = (color >>> 8) & 255\n const blue = color & 255\n return `rgba(${red}, ${green}, ${blue}, ${alpha})`\n }\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n if (trimmed === 'transparent') return 'rgba(0, 0, 0, 0)'\n return trimmed\n}\n\nconst isReanimatedColorHistory = (value: unknown): boolean => {\n 'worklet'\n const normalized = toReanimatedColor(value)\n return typeof normalized === 'string' && normalized.startsWith('rgba(')\n}\n\nconst normalizeAnimationColor = (value: unknown): string | undefined => {\n if (typeof value === 'number') {\n return toReanimatedColor(value) as string\n }\n if (typeof value !== 'string') return\n\n const color = normalizeColor(value)\n if (color == null) return\n const red = Math.round((color & 0xff000000) >>> 24)\n const green = Math.round((color & 0x00ff0000) >>> 16)\n const blue = Math.round((color & 0x0000ff00) >>> 8)\n const alpha = (color & 0x000000ff) / 255\n return `rgba(${red}, ${green}, ${blue}, ${alpha})`\n}\n\nconst buildSnapshot = (\n animated: Record<string, unknown>,\n statics: Record<string, unknown>,\n transforms: Array<Record<string, unknown>>,\n previousKeys: Set<string>,\n lastPainted: Record<string, unknown>,\n derivesLeading = false\n): {\n value: AnimationSnapshot\n painted: Record<string, unknown>\n keys: Set<string>\n} => {\n const snapshotAnimated: Record<string, unknown> = {}\n const snapshotStatics = cloneStyleRecord(statics)\n\n for (const key in snapshotStatics) {\n if (!COLOR_STYLE_KEYS[key]) continue\n const normalized = normalizeAnimationColor(snapshotStatics[key])\n if (normalized !== undefined) snapshotStatics[key] = normalized\n }\n\n for (const key in animated) {\n if (key === 'transform') continue\n const value = animated[key]\n if (COLOR_STYLE_KEYS[key]) {\n const normalized = normalizeAnimationColor(value)\n if (normalized === undefined) {\n // invalid colors must paint plainly. creating a descriptor makes\n // reanimated's string interpolator emit '<letters>NaN'.\n snapshotStatics[key] = cloneAnimationValue(value)\n continue\n }\n snapshotAnimated[key] = normalized\n } else {\n snapshotAnimated[key] = cloneAnimationValue(value)\n }\n }\n\n const snapshotTransforms = transforms.map(cloneStyleRecord)\n const keys = new Set(Object.keys(snapshotAnimated))\n if (derivesLeading) keys.add('lineHeight')\n for (const transform of snapshotTransforms) {\n const key = Object.keys(transform)[0]\n const value = key ? transform[key] : undefined\n if (key && (typeof value === 'number' || typeof value === 'string')) {\n keys.add(`transform:${key}`)\n }\n }\n\n const seeds: Record<string, unknown> = {}\n const staticTransforms = getAnimatedTransforms(snapshotStatics.transform)\n for (const transform of staticTransforms) {\n const key = Object.keys(transform)[0]\n if (keys.has(`transform:${key}`) && previousKeys.has(`transform:${key}`))\n seeds[`transform:${key}`] = transform[key]\n }\n for (const key of keys) {\n // React carries the first animated value until the mapper owns the key.\n // later commits can coalesce before its first run, so their destinations\n // are not evidence of a painted predecessor.\n let value = previousKeys.has(key)\n ? (seeds[key] ?? snapshotStatics[key] ?? lastPainted[key])\n : lastPainted[key]\n if (typeof value !== 'number' && typeof value !== 'string') {\n delete seeds[key]\n continue\n }\n if (typeof value === 'string' && NON_ANIMATABLE_VALUE.test(value)) {\n delete seeds[key]\n continue\n }\n if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value)\n if (value !== undefined) seeds[key] = value\n }\n\n const gatedKeys: Record<string, boolean> = {}\n for (const key of keys) gatedKeys[key] = true\n // a derived leading is a product the mapper computes, not an animation with a\n // completion callback, so no enter/exit cycle can wait on it\n if (derivesLeading) delete gatedKeys.lineHeight\n\n const removedKeys = getRemovedAnimatedKeys(keys, previousKeys)\n const painted: Record<string, unknown> = {\n ...snapshotStatics,\n ...snapshotAnimated,\n }\n painted.transform = snapshotTransforms.length ? snapshotTransforms : staticTransforms\n for (const transform of staticTransforms) {\n const key = Object.keys(transform)[0]\n if (key) painted[`transform:${key}`] = transform[key]\n }\n for (const transform of snapshotTransforms) {\n const key = Object.keys(transform)[0]\n if (key) painted[`transform:${key}`] = transform[key]\n }\n\n return {\n value: {\n animated: snapshotAnimated,\n statics: snapshotStatics,\n transforms: snapshotTransforms,\n gatedKeys,\n derivesLeading,\n seeds,\n removedKeys,\n removeTransform: Object.keys(removedKeys).some((key) =>\n key.startsWith('transform:')\n ),\n clearValue: isWeb ? '' : null,\n },\n painted,\n keys,\n }\n}\n\nconst getGatedKeys = (snapshot: AnimationSnapshot): string[] =>\n Object.keys(snapshot.gatedKeys)\n\nconst createReanimatedConfig = (config: TransitionConfig): Record<string, unknown> => {\n 'worklet'\n const next: Record<string, unknown> = {}\n const source = config as Record<string, unknown>\n\n for (const key in source) {\n if (key === 'type' || key === 'delay') continue\n const value = source[key]\n if (value !== undefined) {\n next[key] = value\n }\n }\n\n return next\n}\n\n/**\n * Apply animation to a value based on config, with optional completion callback\n */\nconst applyAnimation = <T extends number | string>(\n targetValue: T,\n config: TransitionConfig,\n callback?: AnimationCallback,\n seedValue?: number | string,\n validateStartAsColor = false,\n publishTo?: SharedValue<number>\n): T => {\n 'worklet'\n const delay = config.delay\n const reanimatedConfig = createReanimatedConfig(config)\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(\n targetValue as number,\n reanimatedConfig as WithTimingConfig,\n callback\n )\n } else {\n animatedValue = withSpring(\n targetValue as number,\n reanimatedConfig as WithSpringConfig,\n callback\n )\n\n // reanimated stamps lastTimestamp from performance.now() when an animation\n // starts outside a frame (an event handler), then steps it on the frame\n // timestamp, which is the frame's start and lags performance.now() badly\n // when frames are starved. the delta goes negative, and the closed-form\n // spring exponentiates instead of decaying: a tooltip crossing triggers on\n // a loaded machine lands at translate -33,554,430px for a frame before it\n // relaxes back. reanimated clamps the delta's upper bound only, so hold the\n // clock monotonic before it reaches the spring math.\n const innerOnFrame = animatedValue.onFrame\n animatedValue.onFrame = (animation: any, now: number) => {\n 'worklet'\n return innerOnFrame(\n animation,\n now < animation.lastTimestamp ? animation.lastTimestamp : now\n )\n }\n }\n\n // reanimated starts a descriptor from its per-view history for the key — the\n // key's last output value, or the descriptor's own toValue when there is\n // none (an instant snap). both are wrong for a key that turns animated with\n // a painted predecessor: history may hold a stale clear-value ('') from when\n // the key was last removed, which poisons the start value (NaN frames). the\n // wrapper substitutes the seed for whatever start value reanimated passes.\n // color history gets the same treatment only when reanimated's own parser\n // rejects it, preserving valid in-flight values during interruption.\n //\n // guard: when reanimated's IN_STYLE_UPDATER flag is stuck true (e.g. after\n // a throw during initialUpdaterRun), withSpring/withTiming return the raw\n // target value (number or string) instead of an animation descriptor.\n // bail out instead of crashing on .onStart assignment. (#4193)\n //\n // note: we avoid `typeof x === 'object'` here because SWC's ES5 transform\n // replaces it with a `_type_of()` helper that isn't a worklet — calling it\n // on the UI Runtime crashes Reanimated 4. checking for known primitive types\n // instead is safe because SWC leaves those comparisons as plain `typeof`.\n const isAnimationDescriptor =\n animatedValue !== null &&\n typeof animatedValue !== 'number' &&\n typeof animatedValue !== 'string' &&\n typeof animatedValue !== 'boolean' &&\n typeof animatedValue !== 'undefined'\n\n if (isAnimationDescriptor && delay && delay > 0) {\n animatedValue = withDelay(delay, animatedValue)\n }\n\n if (isAnimationDescriptor && (seedValue !== undefined || validateStartAsColor)) {\n const innerOnStart = animatedValue.onStart\n animatedValue.onStart = (\n animation: unknown,\n value: unknown,\n timestamp: number,\n previousAnimation: unknown\n ) => {\n 'worklet'\n const startValue = validateStartAsColor\n ? isReanimatedColorHistory(value)\n ? toReanimatedColor(value)\n : (seedValue ?? targetValue)\n : seedValue\n innerOnStart(animation, startValue, timestamp, previousAnimation)\n }\n }\n\n // a published font size mirrors what THIS descriptor paints, frame by frame.\n // descendants that inherit the size read the mirror, so the host's own\n // animation stays the only clock: no shadow animation, no second config, and\n // nothing to keep in sync when this one is interrupted.\n // (withDelay already wrapped above, before the seed onStart override, per\n // main's hold-enterStyle-through-delay fix: applying it again here would\n // double the delay.)\n if (isAnimationDescriptor && publishTo) {\n const startedOn = animatedValue.onStart\n animatedValue.onStart = (\n animation: any,\n value: unknown,\n timestamp: number,\n previousAnimation: unknown\n ) => {\n 'worklet'\n startedOn(animation, value, timestamp, previousAnimation)\n publishTo.value = animation.current\n }\n const framedOn = animatedValue.onFrame\n animatedValue.onFrame = (animation: any, now: number) => {\n 'worklet'\n const finished = framedOn(animation, now)\n publishTo.value = animation.current\n return finished\n }\n }\n\n return animatedValue as T\n}\n\nconst animateSnapshotValue = (\n key: string,\n implicitKey: string,\n targetValue: number | string,\n config: TransitionConfig,\n previouslyEmitted: boolean,\n seedValue: unknown,\n gated: boolean,\n currentlyExiting: boolean,\n exitCycleId: number,\n currentlyCompletingEnter: boolean,\n enterCycleId: number,\n currentlyCompletingUpdate: boolean,\n updateCycleId: number,\n markExitKeyDone: (key: string, cycleId: number, finished: boolean) => void,\n markEnterKeyDone: (key: string, cycleId: number) => void,\n markUpdateKeyDone: (key: string, cycleId: number, finished: boolean) => void,\n validateStartAsColor = false,\n publishTo?: SharedValue<number>\n): number | string => {\n 'worklet'\n\n const cycleGated =\n gated && (currentlyExiting || currentlyCompletingEnter || currentlyCompletingUpdate)\n if (!previouslyEmitted && seedValue === undefined && !cycleGated) {\n // painted plainly, so the mirror takes it straight\n if (publishTo && typeof targetValue === 'number') publishTo.value = targetValue\n return targetValue\n }\n\n let callback: AnimationCallback | undefined\n if (gated && currentlyExiting) {\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false)\n }\n } else if (gated && currentlyCompletingEnter) {\n callback = () => {\n 'worklet'\n runOnJS(markEnterKeyDone)(key, enterCycleId)\n }\n } else if (gated && currentlyCompletingUpdate) {\n callback = (finished) => {\n 'worklet'\n runOnJS(markUpdateKeyDone)(key, updateCycleId, finished ?? false)\n }\n }\n\n return applyAnimation(\n targetValue,\n config,\n callback,\n previouslyEmitted\n ? undefined\n : ((seedValue ?? getImplicitDefault(implicitKey, targetValue)) as number | string),\n validateStartAsColor,\n publishTo\n )\n}\n\n// =============================================================================\n// Animatable Properties\n// =============================================================================\n\nconst ANIMATABLE_PROPERTIES: Record<string, boolean> = {\n // Transform\n transform: true,\n // Opacity\n opacity: true,\n // Dimensions\n height: true,\n width: true,\n minWidth: true,\n minHeight: true,\n maxWidth: true,\n maxHeight: true,\n // Background\n backgroundColor: true,\n // Border colors\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n // Border radius\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n // Border width\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n // Text\n color: true,\n fontSize: true,\n fontWeight: true,\n lineHeight: true,\n letterSpacing: true,\n // Position\n left: true,\n right: true,\n top: true,\n bottom: true,\n // Margin\n margin: true,\n marginTop: true,\n marginBottom: true,\n marginLeft: true,\n marginRight: true,\n marginHorizontal: true,\n marginVertical: true,\n // Padding\n padding: true,\n paddingTop: true,\n paddingBottom: true,\n paddingLeft: true,\n paddingRight: true,\n paddingHorizontal: true,\n paddingVertical: true,\n // Flex/Gap\n gap: true,\n rowGap: true,\n columnGap: true,\n flex: true,\n flexGrow: true,\n flexShrink: true,\n}\n\n/**\n * Check if a style property can be animated\n */\nconst canAnimateProperty = (key: string, value: unknown): boolean => {\n if (!ANIMATABLE_PROPERTIES[key]) return false\n if (typeof value === 'string' && NON_ANIMATABLE_VALUE.test(value)) return false\n return true\n}\n\n// narrowing to a subset of properties is the transition's own job: a key no\n// entry covers gets `instantConfig()` in `buildTransitionConfig` and snaps.\nconst splitAnimationStyles = (\n style: Record<string, unknown>,\n isDark: boolean,\n disableAnimation: boolean\n) => {\n const animated: Record<string, unknown> = {}\n const statics: Record<string, unknown> = {}\n\n for (const key in style) {\n const value = resolveDynamicValue(style[key], isDark)\n if (value === undefined) continue\n\n if (!disableAnimation && canAnimateProperty(key, value)) {\n animated[key] = cloneAnimationValue(value)\n } else {\n statics[key] = cloneAnimationValue(value)\n }\n }\n\n return { animated, statics }\n}\n\n// =============================================================================\n// Animated Components (Web)\n// =============================================================================\n\n/**\n * Create a Tamagui-compatible animated component for web\n * Supports data- attributes, className, and proper style handling\n */\nfunction createWebAnimatedComponent(defaultTag: 'div' | 'span') {\n const isText = defaultTag === 'span'\n\n const Component = Animated.createAnimatedComponent(\n createRefComponent((propsIn: any, ref) => {\n const { forwardedRef, render = defaultTag, ...rest } = propsIn\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)\n\n const stateRef = useRef<{ host: HTMLElement | null }>({\n get host() {\n return hostRef.current\n },\n })\n\n const [, themeState] = useThemeWithState({})\n\n const result = getSplitStyles(\n rest,\n isText ? Text.staticConfig : View.staticConfig,\n themeState?.theme ?? {},\n themeState?.name ?? '',\n { unmounted: false } as any,\n { isAnimated: false, noClass: true }\n )\n\n const viewProps = result?.viewProps ?? {}\n const Element = render\n const transformedProps = hooks.usePropsTransform?.(\n render,\n viewProps,\n stateRef as any,\n false\n )\n const { nativeID, ...webProps } = transformedProps ?? viewProps\n\n return <Element {...webProps} ref={composedRefs} />\n })\n )\n ;(Component as any).acceptRenderProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// a TextInput never inherits font size from an ancestor Text on native, so the\n// binding hook has to hand it a host that accepts animated styles of its own.\n// built on first use: the compiler evaluates tamagui.config.ts against a\n// react-native stub that has no TextInput, and never binds any text.\nlet animatedTextInput: React.ComponentType<any> | undefined\n\n// =============================================================================\n// Transition Config Builder\n// =============================================================================\n\ntype TransitionConfigResult = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n resolved: ResolvedTransition\n}\n\nconst defaultConfig: TransitionConfig = { type: 'spring' }\n\n// one Easing.bezier worklet per easing: a fresh worklet re-serializes to the\n// UI runtime every time configRef is written, and a render or emit rebuilds\n// every key's config (an inline transition object resolves anew each render)\nconst bezierEasings = new Map<string, ReturnType<typeof Easing.bezier> | undefined>()\n\nfunction bezierEasing(easing: string) {\n if (!bezierEasings.has(easing)) {\n // a `linear()` or `steps()` easing has no bezier form, so leave it to\n // reanimated's default rather than applying something that isn't it.\n const bezier = easingToBezier(easing)\n bezierEasings.set(\n easing,\n bezier ? Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) : undefined\n )\n }\n return bezierEasings.get(easing)\n}\n\n/** one resolved entry as reanimated's own animation config */\nfunction entryToReanimated(entry: ResolvedEntry): TransitionConfig {\n if (entry.timing.kind === 'spring') {\n return {\n type: 'spring',\n stiffness: entry.timing.stiffness,\n damping: entry.timing.damping,\n mass: entry.timing.mass,\n delay: entry.delayMs || undefined,\n }\n }\n return {\n type: 'timing',\n duration: entry.timing.durationMs,\n // Easing.bezier returns a worklet, which is what withTiming wants\n easing: bezierEasing(entry.timing.easing),\n delay: entry.delayMs || undefined,\n }\n}\n\n/**\n * Builds animation config from a transition prop.\n * Shared logic used in both initial render and style emitter updates.\n *\n * Everything meaningful happens in `resolveTransition`, which the other three\n * drivers also call. This only translates the result into reanimated's shape.\n */\nfunction buildTransitionConfig(\n transition: any,\n animations: AnimationsConfig,\n animationState: 'enter' | 'exit' | 'default',\n styleKeys: Set<string>\n): TransitionConfigResult {\n const resolved = forAnimationState(\n resolveTransition(transition, { animations: animations as Record<string, unknown> }),\n animationState\n )\n\n const base = resolved.all ? entryToReanimated(resolved.all) : { ...defaultConfig }\n\n const propertyConfigs: Record<string, TransitionConfig> = {}\n for (const key of styleKeys) {\n const entry = getTransitionForKey(resolved, key)\n // a key no entry covers must SNAP, not animate with some default spring.\n // `transition={{ opacity: '150ms' }}` names opacity and nothing else, and\n // that has to mean the same thing here as it does in a stylesheet.\n propertyConfigs[key] = entry ? entryToReanimated(entry) : instantConfig()\n }\n\n return { baseConfig: base, propertyConfigs, resolved }\n}\n\nconst instantConfig = (): TransitionConfig => ({ type: 'timing', duration: 0 })\n\ntype DriverConfig = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n disableAnimation: boolean\n isHydrating: boolean\n}\n\n// configs are flat (numbers plus a shared easing worklet), so a shallow\n// compare is exact\nfunction sameTransitionConfig(a: TransitionConfig, b: TransitionConfig) {\n const aKeys = Object.keys(a)\n if (aKeys.length !== Object.keys(b).length) return false\n for (const key of aKeys) {\n if (a[key as keyof TransitionConfig] !== b[key as keyof TransitionConfig])\n return false\n }\n return true\n}\n\nfunction sameDriverConfig(a: DriverConfig, b: DriverConfig) {\n if (a.disableAnimation !== b.disableAnimation || a.isHydrating !== b.isHydrating) {\n return false\n }\n if (!sameTransitionConfig(a.baseConfig, b.baseConfig)) return false\n const aKeys = Object.keys(a.propertyConfigs)\n if (aKeys.length !== Object.keys(b.propertyConfigs).length) return false\n for (const key of aKeys) {\n const other = b.propertyConfigs[key]\n if (!other || !sameTransitionConfig(a.propertyConfigs[key], other)) return false\n }\n return true\n}\n\n/**\n * Extracts all style keys including transform sub-properties.\n */\nfunction getStyleKeys(style: Record<string, unknown>): Set<string> {\n const keys = new Set(Object.keys(style))\n if (style.transform && Array.isArray(style.transform)) {\n for (const t of style.transform as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n keys.add(Object.keys(t)[0])\n }\n }\n }\n return keys\n}\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Tamagui.\n *\n * This is a native Reanimated implementation without Moti dependency.\n * It provides smooth spring and timing animations with full support for:\n * - Per-property animation configurations\n * - Exit animations with proper completion callbacks\n * - Dynamic theme value resolution\n * - Transform property animations\n * - avoidReRenders optimization for hover/press/focus states\n *\n * @example\n * ```tsx\n * const animations = createAnimations({\n * fast: { duration: 200, bounce: 0.2 },\n * slow: { type: 'timing', duration: 500, easing: 'ease-out' },\n * })\n * ```\n */\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A\n): AnimationDriverWithAnimatedNumbers<A> {\n return {\n needsCustomComponent: true,\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n inputStyle: 'value',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n // binds a descendant that has no font size of its own to the ancestor's\n // animated one. no clock here: the worklet reads the ancestor's shared\n // value, so the font size and a ratio leading land on the same frame.\n useTextMetrics: ({ inheritedText, lineHeight }) => {\n const node =\n inheritedText?.driver === 'reanimated'\n ? (inheritedText.fontSize as SharedValue<number>)\n : null\n // an absolute or `normal` leading still needs the font size bound, it\n // just keeps the leading the caller already resolved.\n const ratio = typeof lineHeight === 'number' ? lineHeight : null\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n if (!node) return {}\n const fontSize = node.value\n return ratio === null ? { fontSize } : { fontSize, lineHeight: fontSize * ratio }\n }, [node, ratio])\n const textChannel = inheritedText ?? null\n // TextInputHost is absent only in the web build, which core never hands a\n // channel, so this is the same condition as `node` twice over.\n if (!node || !TextInputHost) return { style: null, textChannel }\n animatedTextInput ||= Animated.createAnimatedComponent(TextInputHost)\n return {\n // reanimated's animated style is an opaque handle its own hosts read,\n // not a plain object, so it does not fit the contract's Record shape.\n style: animatedStyle as unknown as Record<string, unknown>,\n textChannel,\n Text: isWeb ? AnimatedText : Animated.Text,\n TextInput: animatedTextInput,\n }\n },\n\n // =========================================================================\n // useAnimatedNumber - For imperative animated values\n // =========================================================================\n useAnimatedNumber(initial): UniversalAnimatedNumber<ReanimatedAnimatedNumber> {\n const sharedValue = useSharedValue(initial)\n\n return useMemo(\n () => ({\n getInstance() {\n 'worklet'\n return sharedValue\n },\n\n getValue() {\n 'worklet'\n return sharedValue.value\n },\n\n setValue(next, config = { type: 'spring' }, onFinish) {\n if (config.type === 'direct') {\n sharedValue.value = next\n onFinish?.()\n } else {\n // animations must start on the ui thread in reanimated 4.x -\n // withSpring/withTiming return animation descriptors that only\n // work when assigned to a SharedValue inside the UI runtime\n const cb = onFinish\n ? (finished?: boolean) => {\n 'worklet'\n if (finished !== false) {\n runOnJS(onFinish)()\n }\n }\n : undefined\n\n const animationConfig = cloneTransitionConfig(config)\n\n if (isWeb) {\n // on web there's no UI thread - set directly\n sharedValue.value = applyAnimation(next, animationConfig, cb)\n } else {\n runOnUI((targetValue: number, animationConfig: TransitionConfig) => {\n 'worklet'\n sharedValue.value = applyAnimation(targetValue, animationConfig, cb)\n })(next, animationConfig)\n }\n }\n },\n\n stop() {\n cancelAnimation(sharedValue)\n },\n }),\n [sharedValue]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberReaction - React to animated value changes\n // =========================================================================\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance()\n\n return useAnimatedReaction(\n () => instance.value,\n (next, prev) => {\n if (prev !== next) {\n runOnJS(onValue)(next)\n }\n },\n [onValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberStyle - Create animated styles from values\n // =========================================================================\n useAnimatedNumberStyle(val, getStyle) {\n const instance = val.getInstance()\n\n if (isWeb) {\n // web: must pass explicit deps (no babel plugin)\n return useAnimatedStyle(() => {\n 'worklet'\n return getStyle(instance.value)\n }, [instance, getStyle])\n }\n\n const styleVal = useDerivedValue(() => {\n 'worklet'\n return getStyle(instance.value)\n })\n\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n return styleVal.value\n })\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n return animatedStyle\n },\n\n useAnimatedNumbersStyle(vals, getStyle) {\n const instances = vals.map((v) => v.getInstance())\n\n const animatedStyle = useAnimatedStyle(\n () => {\n 'worklet'\n const currentValues = instances.map((inst) => inst.value)\n return getStyle(...currentValues)\n },\n isWeb ? [getStyle, ...instances] : undefined\n )\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n return animatedStyle\n },\n\n // =========================================================================\n // useAnimations - Main animation hook for components\n // =========================================================================\n useAnimations(animationProps) {\n const {\n props,\n presence,\n style,\n componentState,\n useStyleEmitter,\n themeName,\n stateRef,\n styleState,\n onTransition,\n inheritedText,\n } = animationProps\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n\n // Track if we just finished entering (transition from entering to not entering)\n const wasEnteringRef = useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n // Use effectiveTransition computed by createComponent (single source of truth)\n const effectiveTransition = styleState?.effectiveTransition ?? props.transition\n // core reports a numeric (ratio) leading on nativeTextMetrics; a px\n // length and 'normal' are reported as themselves and stay independent.\n const leadingRatio = getLeadingRatio(styleState?.nativeTextMetrics)\n\n // Use 'enter' if we're mounting OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n const resolvedTransition = forAnimationState(\n resolveTransition(effectiveTransition, {\n animations: animations as Record<string, unknown>,\n }),\n animationState\n )\n\n const disableAnimation = isHydrating || !hasTransition(resolvedTransition)\n\n // this subtree's live font size, mirrored off whatever the host's own\n // fontSize animation paints (see applyAnimation): one clock, not two.\n // descendants read it, and so does this node's own leading whenever that\n // leading is a ratio of the size.\n const fontSizeShared = useSharedValue(\n typeof style.fontSize === 'number' ? style.fontSize : 0\n )\n const publishesFontSize = !disableAnimation && typeof style.fontSize === 'number'\n const ownTextChannel = useMemo(\n () => ({ fontSize: fontSizeShared, driver: 'reanimated' }),\n [fontSizeShared]\n )\n // an inherited font size: core leaves this node's own fontSize out of the\n // style and reports the ratio, so the leading is the ANCESTOR's live font\n // size times this node's ratio, read straight off the ancestor's mirror.\n const inheritedFontSize =\n leadingRatio !== undefined &&\n typeof style.fontSize !== 'number' &&\n inheritedText?.driver === 'reanimated'\n ? (inheritedText.fontSize as SharedValue<number>)\n : null\n // the font size the leading is a ratio OF: an ancestor's when this node\n // has none of its own, otherwise the one this node paints.\n const leadingFontSize = inheritedFontSize ?? fontSizeShared\n // the leading's other factor. the ratio is this node's own property, so it\n // rides the entry that names lineHeight while the font size stays on its\n // own clock, and the product is exact at every frame either way.\n const leadingRatioShared = useSharedValue(leadingRatio ?? 0)\n // the ratio the painted leading was last built from. `undefined` means the\n // leading was a length or `normal`, which has no ratio to move from, so\n // the next ratio starts the product instead of ramping into it.\n const paintedRatioRef = useRef(leadingRatio)\n const applyLeadingRatio = (ratio: number | undefined, config: TransitionConfig) => {\n const previous = paintedRatioRef.current\n if (previous === ratio) return\n paintedRatioRef.current = ratio\n if (ratio === undefined) return\n leadingRatioShared.value =\n previous === undefined || disableAnimation\n ? ratio\n : applyAnimation(ratio, config)\n }\n\n // Theme state for dynamic values - use themeName from props instead of hook\n const isDark = themeName?.startsWith('dark') || false\n\n // Get sendExitComplete callback from presence\n const sendExitComplete = presence?.[1]\n const onTransitionRef = useRef(onTransition)\n onTransitionRef.current = onTransition\n const emit = (\n phase: 'start' | 'end',\n cause: 'enter' | 'exit' | 'update',\n finished?: boolean\n ) => {\n onTransitionRef.current?.(\n phase === 'end' ? { phase, cause, finished } : { phase, cause }\n )\n }\n const enterStartedRef = useRef(false)\n const exitStartedRef = useRef(false)\n const updateStartedRef = useRef(false)\n const isExitingJSRef = useRef(false)\n isExitingJSRef.current = isExiting\n\n // =========================================================================\n // Update cycle state: an in-place style transition while mounted (not\n // enter/exit). drives the onTransition 'update' lifecycle events and the\n // components (e.g. accordion HeightAnimator) that retain content through\n // an in-place transition.\n // =========================================================================\n const updateCycleIdRef = useRef(0)\n const pendingUpdateKeysRef = useRef<Set<string>>(new Set())\n const updateCompletedRef = useRef(false)\n const isCompletingUpdateRef = useSharedValue(false)\n const updateCycleIdShared = useSharedValue(0)\n // an interrupted key still resolves its participation in this cycle. it\n // used to be dropped on the floor, which left the key in the pending set\n // forever and hung the update cycle: no 'end' event, and any component\n // waiting on one (accordion's HeightAnimator) stayed latched.\n const updateInterruptedRef = useRef(false)\n const markUpdateKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n if (cycleId !== updateCycleIdRef.current) return\n if (updateCompletedRef.current) return\n if (!finished) updateInterruptedRef.current = true\n pendingUpdateKeysRef.current.delete(key)\n if (pendingUpdateKeysRef.current.size === 0) {\n updateCompletedRef.current = true\n isCompletingUpdateRef.value = false\n if (updateStartedRef.current) {\n updateStartedRef.current = false\n emit('end', 'update', !updateInterruptedRef.current)\n }\n }\n }\n )\n\n // =========================================================================\n // Exit cycle state for deterministic per-property completion tracking\n // =========================================================================\n const exitCycleIdRef = useRef(0)\n const pendingExitKeysRef = useRef<Set<string>>(new Set())\n const exitCompletedRef = useRef(false)\n const wasExitingRef = useRef(false)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // stable callback to mark a property as done (called from worklet via runOnJS)\n const markExitKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n // ignore callbacks from stale cycles\n if (cycleId !== exitCycleIdRef.current) return\n // ignore if already completed\n if (exitCompletedRef.current) return\n // count both finished and canceled animations as \"done\" during exit\n // (element is leaving anyway, canceled animations shouldn't block completion)\n\n pendingExitKeysRef.current.delete(key)\n\n // check if all exit animations are done\n if (pendingExitKeysRef.current.size === 0) {\n exitCompletedRef.current = true\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n // exit 'end' fires immediately before presence safeToRemove\n emit('end', 'exit', true)\n }\n sendExitComplete?.()\n }\n }\n )\n\n // =========================================================================\n // Enter cycle state for the onTransition enter 'end' event\n // =========================================================================\n const enterCycleIdRef = useRef(0)\n const pendingEnterKeysRef = useRef<Set<string>>(new Set())\n const enterCompletedRef = useRef(false)\n\n const markEnterKeyDone = useEvent((key: string, cycleId: number) => {\n if (cycleId !== enterCycleIdRef.current) return\n if (enterCompletedRef.current) return\n if (isExitingJSRef.current) return\n\n pendingEnterKeysRef.current.delete(key)\n\n if (pendingEnterKeysRef.current.size === 0) {\n enterCompletedRef.current = true\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n }\n })\n\n // SharedValue to pass exit state into worklet\n const isExitingRef = useSharedValue(isExiting)\n const exitCycleIdShared = useSharedValue(exitCycleIdRef.current)\n const isCompletingEnterRef = useSharedValue(false)\n const enterCycleIdShared = useSharedValue(enterCycleIdRef.current)\n\n // start new exit cycle only on transition INTO exiting (not every render while exiting)\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n pendingExitKeysRef.current.clear()\n enterCycleIdRef.current++\n pendingEnterKeysRef.current.clear()\n updateCycleIdRef.current++\n pendingUpdateKeysRef.current.clear()\n updateCompletedRef.current = true\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n pendingExitKeysRef.current.clear()\n }\n\n // update shared values (using effect to avoid writing during render)\n useIsomorphicLayoutEffect(() => {\n isExitingRef.value = isExiting\n exitCycleIdShared.value = exitCycleIdRef.current\n isCompletingEnterRef.value =\n !isExiting && justFinishedEntering && Boolean(onTransitionRef.current)\n enterCycleIdShared.value = enterCycleIdRef.current\n if (justStartedExiting) {\n isCompletingUpdateRef.value = false\n updateCycleIdShared.value = updateCycleIdRef.current\n }\n }, [\n isExiting,\n justFinishedEntering,\n exitCycleIdRef.current,\n enterCycleIdRef.current,\n justStartedExiting,\n ])\n\n // exit interrupted by a re-enter: report the exit as finished:false\n useIsomorphicLayoutEffect(() => {\n if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {\n exitStartedRef.current = false\n emit('end', 'exit', false)\n }\n }, [justStoppedExiting])\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\n })\n\n // =========================================================================\n // avoidRerenders: SharedValues for style updates without re-renders\n // =========================================================================\n const committedRenderKeysRef = useRef(new Set<string>())\n // full painted style of the last committed source (render or emitter):\n // statics + animated targets, transform sub-keys flattened as `transform:key`.\n // seeds read from it.\n const lastPaintedRef = useRef<Record<string, unknown>>({})\n // per-key first-animated values React keeps painting under the mapper —\n // see the styles memo below\n const carryRef = useRef<Record<string, unknown>>({})\n\n // commits during exit must keep the predecessor from before exit began,\n // even if they publish again before the UI mapper consumes the first one.\n const exitSeedRef = useRef<Record<string, unknown> | null>(null)\n const paintedPredecessor = isExiting\n ? (exitSeedRef.current ?? lastPaintedRef.current)\n : lastPaintedRef.current\n useIsomorphicLayoutEffect(() => {\n exitSeedRef.current = isExiting ? paintedPredecessor : null\n }, [isExiting, paintedPredecessor])\n\n // Separate styles into animated and static\n const { animatedStyles, staticStyles, nextCarry } = useMemo(() => {\n const { animated, statics } = splitAnimationStyles(\n style as Record<string, unknown>,\n isDark,\n disableAnimation\n )\n\n // retain the predecessor's composition when exit only targets some transforms.\n if (isExiting && animated.transform) {\n const remaining = getAnimatedTransforms(animated.transform).map(\n cloneStyleRecord\n )\n const previous = getAnimatedTransforms(paintedPredecessor.transform)\n if (previous.length) {\n animated.transform = previous\n .map((entry) => {\n const key = Object.keys(entry)[0]\n const index = remaining.findIndex((target) => key in target)\n return index < 0 ? cloneStyleRecord(entry) : remaining.splice(index, 1)[0]\n })\n .concat(remaining)\n }\n }\n\n // every animated key keeps its FIRST animated value in React's style for\n // as long as it stays animated. the value never changes across renders,\n // so React's per-key style diff never touches the key again and the\n // mapper's inline writes survive every commit — reanimated's Fabric\n // commit hook provides this guarantee on native, web has no equivalent.\n // during enter the carry is the enter-clause value (the mapper animates over it);\n // a key appearing post-mount (a just-measured height) paints its value in\n // the same commit the mapper first sees it, a frame before the mapper's\n // first write.\n // derive from committed state. mutating carryRef during render leaks\n // abandoned concurrent renders into later commits.\n const nextCarry = cloneStyleRecord(carryRef.current)\n for (const key in nextCarry) {\n if (!(key in animated)) delete nextCarry[key]\n }\n for (const key in animated) {\n if (!(key in nextCarry)) {\n const target = animated[key]\n if (isExiting && key === 'transform') {\n nextCarry[key] = getAnimatedTransforms(target).map((transform) => {\n const initial = cloneStyleRecord(transform)\n for (const part in initial) {\n const value = initial[part]\n if (typeof value === 'number' || typeof value === 'string') {\n initial[part] =\n paintedPredecessor[`transform:${part}`] ??\n getImplicitDefault(part, value)\n }\n }\n return initial\n })\n } else {\n nextCarry[key] = cloneAnimationValue(\n isExiting && (typeof target === 'number' || typeof target === 'string')\n ? (paintedPredecessor[key] ?? getImplicitDefault(key, target))\n : target\n )\n }\n }\n statics[key] = nextCarry[key]\n }\n\n return { animatedStyles: animated, staticStyles: statics, nextCarry }\n }, [\n disableAnimation,\n style,\n isDark,\n isExiting,\n isExiting ? paintedPredecessor : null,\n ])\n\n const derivesLeading = derivesLeadingProduct(\n animatedStyles,\n leadingRatio,\n !!inheritedFontSize,\n resolvedTransition\n )\n\n const renderSnapshot = useMemo(\n () =>\n buildSnapshot(\n animatedStyles,\n staticStyles,\n getAnimatedTransforms(animatedStyles.transform),\n committedRenderKeysRef.current,\n paintedPredecessor,\n derivesLeading\n ),\n [\n animatedStyles,\n staticStyles,\n isExiting ? paintedPredecessor : null,\n derivesLeading,\n ]\n )\n const renderSnapshotRef = useSharedValue<AnimationSnapshot>({\n animated: {},\n statics: {},\n transforms: [],\n gatedKeys: {},\n derivesLeading: false,\n seeds: {},\n removedKeys: {},\n removeTransform: false,\n clearValue: isWeb ? '' : null,\n })\n const emitterSnapshotRef = useSharedValue<AnimationSnapshot | null>(null)\n const emitterKeysRef = useRef<Set<string> | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n carryRef.current = nextCarry\n committedRenderKeysRef.current = renderSnapshot.keys\n lastPaintedRef.current = renderSnapshot.painted\n }, [nextCarry, renderSnapshot])\n\n // reconcile the emitter latch with real re-renders.\n //\n // the avoidReRenders fast path lets a pure pseudo change (hover/press/focus) push\n // styles straight to the worklet via useStyleEmitter with no React commit — it sets\n // emitterSnapshotRef. the worklet then treats `emitterSnapshotRef !== null` as a\n // permanent latch: once the emitter has fired even once, it reads its last-emitted\n // snapshot and IGNORES `animatedStyles` from every subsequent re-render. so a base\n // style that changes through React (e.g. a row's `backgroundColor` flipping with an\n // external selection store, not a hover) never reaches the screen — it stays stuck on\n // the stale emitted value until the next hover re-fires the emitter. (the \"active row\n // highlight gets stuck until you hover it again\" report.)\n //\n // a real re-render is the freshest source of truth for the base style, so on render we\n // drop the emitter latch and let the worklet fall back to `animatedStyles`. the one\n // exception: while a self pseudo (hover/press/focus) is active the emitted style is a\n // transient override that isn't in this render, so keep it latched — otherwise an\n // unrelated re-render while hovering would snap back to the base. `pseudoActiveRef`\n // tracks that flag straight off the emitter (which fires on every pseudo transition), so\n // the un-hover/un-press emission flips it false and the next render reclaims the style.\n // no value comparison needed; runtime media stays correct because a render re-reads live\n // media into `animatedStyles`.\n // exit overrides the pseudo exception: the exit render's style IS the\n // authoritative target, and an element that exits while hovered/focused (every\n // dialog closed by clicking a button inside it) would otherwise keep the stale\n // emitted base latched — the worklet then \"animates\" to values already on\n // screen, every exit-key callback completes in a frame, and AnimatePresence\n // unmounts immediately (the dialog-exit-snap). mirrored into a plain ref so the\n // emitter callback (which runs outside render) can read it.\n const pseudoActiveRef = useRef(false)\n // the committed render is the worklet's source of truth whenever the emitter is not\n // latched, so every render must publish its snapshot — not just the renders that drop\n // a latch. animated keys live only in this snapshot after mount (staticStyles carries\n // them during mount only), so a render that never publishes leaves the worklet reading\n // an empty snapshot and the animated properties never reach the screen at all.\n // The first emitter snapshot can already contain the browser's real media-query state\n // while the first render still contains mediaQueryDefaultActive for hydration. Publish\n // that render behind the latch, but do not let it replace the fresher runtime snapshot.\n const publishedSnapshotRef = useRef<object | null>(null)\n useIsomorphicLayoutEffect(() => {\n const renderChanged = publishedSnapshotRef.current !== renderSnapshot.value\n const droppingLatch =\n (isExiting ||\n (!pseudoActiveRef.current &&\n publishedSnapshotRef.current !== null &&\n renderChanged)) &&\n emitterSnapshotRef.value !== null\n // when the latch drops, keys the emitter owned that this render no longer has must\n // be cleared too, otherwise reanimated keeps painting the stale emitted value\n const emitterKeys = droppingLatch ? emitterKeysRef.current : null\n\n if (droppingLatch || renderChanged) {\n const removedKeys = emitterKeys\n ? {\n ...renderSnapshot.value.removedKeys,\n ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys),\n }\n : renderSnapshot.value.removedKeys\n renderSnapshotRef.value = {\n ...renderSnapshot.value,\n removedKeys,\n removeTransform: Object.keys(removedKeys).some((key) =>\n key.startsWith('transform:')\n ),\n }\n publishedSnapshotRef.current = renderSnapshot.value\n }\n\n if (droppingLatch) {\n emitterSnapshotRef.value = null\n emitterKeysRef.current = null\n }\n })\n\n // Build animation config with per-property overrides using normalized transition\n const { baseConfig, propertyConfigs } = useMemo(() => {\n if (isHydrating) {\n return {\n baseConfig: { type: 'timing' as const, duration: 0 },\n propertyConfigs: {} as Record<string, TransitionConfig>,\n }\n }\n\n // use effectiveTransition (createComponent's single source of truth) so a\n // platform-pseudo component with no declared transition resolves its BASE\n // config to instant ('0ms') too — not just its pseudo flips. otherwise base\n // style changes on the driver path animate with the default spring and a\n // hover-out / base color change visibly lingers (looks \"stuck\").\n return buildTransitionConfig(\n effectiveTransition,\n animations,\n animationState,\n getStyleKeys(animatedStyles)\n )\n }, [isHydrating, effectiveTransition, animatedStyles, animationState])\n\n // Store config in SharedValue for worklet access (concurrent-safe)\n // Using useEffect to avoid writing to shared value during render\n const renderConfig: DriverConfig = {\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n }\n const configRef = useSharedValue(renderConfig)\n // the last config written from JS. every write serializes it to the UI\n // runtime and wakes the style mapper, and a render rebuilds the config\n // whenever animatedStyles changes identity, so writes that change\n // nothing are skipped (the mount write repeats the seed above)\n const writtenConfigRef = useRef(renderConfig)\n const writeConfig = (next: DriverConfig) => {\n if (sameDriverConfig(writtenConfigRef.current, next)) return\n writtenConfigRef.current = next\n configRef.value = next\n }\n\n useIsomorphicLayoutEffect(() => {\n writeConfig(renderConfig)\n }, [baseConfig, propertyConfigs, disableAnimation, isHydrating])\n\n // only the style the mapper is actually reading may move the ratio: while\n // an emitted pseudo style is latched it owns the leading, and the emitter\n // has already written the ratio that goes with it.\n useIsomorphicLayoutEffect(() => {\n if (emitterSnapshotRef.value !== null) return\n applyLeadingRatio(leadingRatio, propertyConfigs.lineHeight ?? baseConfig)\n })\n\n // =========================================================================\n // avoidRerenders: register style emitter callback\n // when hover/press/etc state changes, this is called instead of re-rendering\n // =========================================================================\n // the fourth argument is the emitted style's own nativeTextMetrics: the\n // emitter re-runs getSplitStyles outside render, so the ratio that\n // describes the style it hands over is never the ratio the last render\n // resolved (a pseudo can replace the leading with an absolute length).\n const onEmittedStyle = (\n nextStyle: Record<string, unknown>,\n effectiveTransition: TransitionProp | null | undefined,\n pseudoActive?: boolean,\n nextMetrics?: NativeTextMetrics\n ) => {\n // while exiting, the exit render owns the style — a pseudo flip mid-exit\n // (hover-out as the element fades under the cursor) must not re-latch the\n // base style over the in-flight exit targets.\n if (isExitingJSRef.current) return\n // track whether a self pseudo is active so the render-time layout effect knows whether\n // this emitter snapshot is a transient override to keep latched or a base it can drop.\n pseudoActiveRef.current = pseudoActive === true\n\n // effectiveTransition is computed in createComponent based on entering/exiting pseudo states\n // rebuild config whenever transition changes (entering OR exiting pseudo states)\n const transitionToUse = effectiveTransition ?? props.transition\n const emittedLeadingRatio = getLeadingRatio(nextMetrics)\n const {\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n resolved: emittedResolved,\n } = buildTransitionConfig(\n transitionToUse,\n animations,\n animationState,\n getStyleKeys(nextStyle)\n )\n\n const written = writtenConfigRef.current\n writeConfig({\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n disableAnimation: written.disableAnimation,\n isHydrating: written.isHydrating,\n })\n\n const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current\n const { animated, statics } = splitAnimationStyles(\n nextStyle,\n isDark,\n written.disableAnimation\n )\n // the emitted style resolves its own leading: a pseudo can replace a\n // ratio with a length, which unbinds the product for as long as it holds\n const emittedDerivesLeading = derivesLeadingProduct(\n animated,\n emittedLeadingRatio,\n !!inheritedFontSize,\n emittedResolved\n )\n applyLeadingRatio(emittedLeadingRatio, newPropertyConfigs.lineHeight ?? newBase)\n const snapshot = buildSnapshot(\n animated,\n statics,\n getAnimatedTransforms(animated.transform),\n previousKeys,\n lastPaintedRef.current,\n emittedDerivesLeading\n )\n emitterSnapshotRef.value = snapshot.value\n emitterKeysRef.current = snapshot.keys\n lastPaintedRef.current = snapshot.painted\n\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useStyleEmitter update', {\n animated,\n statics,\n transforms: snapshot.value.transforms,\n })\n }\n }\n useStyleEmitter?.(onEmittedStyle)\n\n // Compute and register exit keys synchronously during render to avoid race conditions\n // This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set\n const exitKeysRegistered = useRef(false)\n const enterKeysRegistered = useRef(false)\n\n if (!isExiting && justFinishedEntering && onTransition) {\n const enterKeys = getGatedKeys(renderSnapshot.value)\n enterCycleIdRef.current++\n enterCompletedRef.current = false\n pendingEnterKeysRef.current = new Set(enterKeys)\n enterKeysRegistered.current = enterKeys.length > 0\n }\n\n if (justStartedExiting && sendExitComplete) {\n const exitKeys = getGatedKeys(renderSnapshot.value)\n pendingExitKeysRef.current = new Set(exitKeys)\n exitKeysRegistered.current = exitKeys.length > 0\n }\n\n // update cycle: a genuine in-place style change while mounted (not\n // entering or exiting). register the exact keys that can animate before\n // publishing the cycle to the worklet; shared values are only written\n // from the layout effect, never from the mapper that reads them.\n const previousUpdateStyleRef = useRef<string | null>(null)\n const updateStyle = onTransition ? JSON.stringify(style) : null\n const previousUpdateStyle = previousUpdateStyleRef.current\n const shouldRegisterUpdate =\n updateStyle !== null &&\n previousUpdateStyle !== null &&\n previousUpdateStyle !== updateStyle &&\n !isExiting &&\n !isEntering &&\n !justFinishedEntering\n const updateKeys = shouldRegisterUpdate ? getGatedKeys(renderSnapshot.value) : []\n\n useIsomorphicLayoutEffect(() => {\n if (updateStyle === null) {\n previousUpdateStyleRef.current = null\n isCompletingUpdateRef.value = false\n return\n }\n\n // only a committed render may advance the authoritative cycle. the\n // captured previous value prevents StrictMode's repeated effect from\n // registering the same transition twice.\n const previousStyleStillCurrent =\n previousUpdateStyleRef.current === previousUpdateStyle\n previousUpdateStyleRef.current = updateStyle\n if (!shouldRegisterUpdate || !previousStyleStillCurrent) return\n\n const cycleId = ++updateCycleIdRef.current\n pendingUpdateKeysRef.current = new Set(updateKeys)\n updateInterruptedRef.current = false\n updateCompletedRef.current = updateKeys.length === 0\n isCompletingUpdateRef.value = updateKeys.length > 0\n updateCycleIdShared.value = cycleId\n\n // a superseded in-flight update reports finished:false before the new\n // cycle starts; a zero-key cycle reports an immediate start/end pair\n if (updateStartedRef.current) {\n emit('end', 'update', false)\n }\n updateStartedRef.current = true\n emit('start', 'update')\n if (updateKeys.length === 0) {\n updateStartedRef.current = false\n emit('end', 'update', true)\n }\n }, [updateStyle, shouldRegisterUpdate])\n\n // handle zero-animation case in effect (after render commit)\n React.useEffect(() => {\n if (!justStartedExiting || !sendExitComplete) return\n\n // enter or update still in flight when exit begins is reported as finished:false\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', false)\n }\n if (updateStartedRef.current) {\n updateStartedRef.current = false\n emit('end', 'update', false)\n }\n\n // emit exit start once per exit cycle\n if (onTransitionRef.current && !exitStartedRef.current) {\n exitStartedRef.current = true\n emit('start', 'exit')\n }\n\n // if no keys were registered, complete immediately\n if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {\n if (!exitCompletedRef.current) {\n exitCompletedRef.current = true\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n emit('end', 'exit', true)\n }\n sendExitComplete()\n }\n }\n }, [justStartedExiting, sendExitComplete])\n\n React.useEffect(() => {\n if (isExiting || !justFinishedEntering || !onTransition) return\n\n // emit enter start once per enter cycle\n if (!enterStartedRef.current) {\n enterStartedRef.current = true\n emit('start', 'enter')\n }\n\n if (!enterKeysRegistered.current && pendingEnterKeysRef.current.size === 0) {\n if (!enterCompletedRef.current) {\n enterCompletedRef.current = true\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n }\n }\n }, [isExiting, justFinishedEntering, onTransition])\n\n // the worklet's own record of which animated keys it has emitted (mirrors\n // reanimated's per-view style history, which resets whenever a key leaves\n // the output). a key not in here is fresh to the mapper no matter what\n // React has committed — several renders can coalesce into one mapper run.\n // native updates use modify with the same object and no forced update, so\n // the mapper doesn't react to its own history write.\n const mapperStateRef = useSharedValue<MapperState>({\n emitted: {},\n ownedStatics: {},\n })\n // Create animated style\n const animatedStyle = useAnimatedStyle(\n () => {\n 'worklet'\n\n const mapperState = mapperStateRef.value\n const config = configRef.value\n if (config.disableAnimation || config.isHydrating) {\n // the empty return wipes reanimated's per-key history, so ours\n // resets with it\n updateMapperState(mapperStateRef, {}, {})\n return {}\n }\n\n const previouslyEmitted = mapperState.emitted\n const emitted: Record<string, boolean> = {}\n const previouslyOwned = mapperState.ownedStatics\n const ownedStatics: Record<string, boolean> = {}\n\n const result: Record<string, any> = {}\n\n // Check if we have avoidRerenders updates from useStyleEmitter\n const emitterSnapshot = emitterSnapshotRef.value\n const snapshot = emitterSnapshot ?? renderSnapshotRef.value\n\n // Use emitter values if available, otherwise use the committed render snapshot.\n // statics fall back to the render's staticStyles for keys the mapper\n // owns (see MapperState): once an emitter snapshot has applied a\n // static key, reanimated never unsets it, so after the latch drops a\n // stale emitted value (e.g. a border color that could not animate)\n // would keep painting over the fresh style prop forever\n const animatedValues = snapshot.animated\n const staticValues = snapshot.statics\n\n // read lifecycle state from shared values\n const currentlyExiting = isExitingRef.value\n const currentCycleId = exitCycleIdShared.value\n const currentlyCompletingEnter = isCompletingEnterRef.value\n const currentEnterCycleId = enterCycleIdShared.value\n const currentlyCompletingUpdate = isCompletingUpdateRef.value\n const currentUpdateCycleId = updateCycleIdShared.value\n\n // Include static values from emitter (for hover/press style changes).\n // a render snapshot only refreshes keys an emitter already wrote.\n for (const key in staticValues) {\n if (!emitterSnapshot && !previouslyOwned[key]) continue\n ownedStatics[key] = true\n result[key] = staticValues[key]\n }\n for (const key in snapshot.removedKeys) {\n if (!key.startsWith('transform:') && !(key in staticValues)) {\n // native color processing rejects null. text color must remain\n // absent rather than making the text transparent.\n if (\n snapshot.clearValue === null &&\n (key === 'color' || key.endsWith('Color'))\n ) {\n if (key !== 'color') result[key] = 'transparent'\n } else {\n result[key] = snapshot.clearValue\n }\n }\n }\n\n // Animate regular properties\n for (const key in animatedValues) {\n if (key === 'transform') continue\n\n const targetValue = animatedValues[key]\n emitted[key] = true\n // taken after this loop, so the font size it multiplies is the one\n // this frame paints\n if (key === 'lineHeight' && snapshot.derivesLeading) continue\n if (typeof targetValue !== 'number' && typeof targetValue !== 'string') {\n result[key] = targetValue\n continue\n }\n result[key] = animateSnapshotValue(\n key,\n key,\n targetValue,\n config.propertyConfigs[key] ?? config.baseConfig,\n !!previouslyEmitted[key],\n snapshot.seeds[key],\n !!snapshot.gatedKeys[key],\n currentlyExiting,\n currentCycleId,\n currentlyCompletingEnter,\n currentEnterCycleId,\n currentlyCompletingUpdate,\n currentUpdateCycleId,\n markExitKeyDone,\n markEnterKeyDone,\n markUpdateKeyDone,\n !!COLOR_STYLE_KEYS[key],\n key === 'fontSize' ? fontSizeShared : undefined\n )\n }\n\n // the live font size this leading is a ratio of, times the ratio: a\n // product, so both factors are the ones on screen this frame\n if (snapshot.derivesLeading) {\n emitted.lineHeight = true\n result.lineHeight = leadingFontSize.value * leadingRatioShared.value\n }\n\n // Handle transforms\n const transforms = snapshot.transforms\n\n // Animate transform properties with validation\n if (transforms.length > 0 || snapshot.removeTransform) {\n const validTransforms: Record<string, unknown>[] = []\n\n for (const t of transforms) {\n if (!t) continue\n const keys = Object.keys(t)\n if (keys.length === 0) continue\n const transformKey = keys[0]\n const value = t[transformKey]\n if (typeof value === 'number' || typeof value === 'string') {\n const propConfig =\n config.propertyConfigs[transformKey] ?? config.baseConfig\n\n const subKey = `transform:${transformKey}`\n emitted[subKey] = true\n validTransforms.push({\n [transformKey]: animateSnapshotValue(\n subKey,\n transformKey,\n value,\n propConfig,\n !!previouslyEmitted[subKey],\n snapshot.seeds[subKey],\n !!snapshot.gatedKeys[subKey],\n currentlyExiting,\n currentCycleId,\n currentlyCompletingEnter,\n currentEnterCycleId,\n currentlyCompletingUpdate,\n currentUpdateCycleId,\n markExitKeyDone,\n markEnterKeyDone,\n markUpdateKeyDone\n ),\n })\n } else {\n validTransforms.push(t)\n }\n }\n\n if (validTransforms.length > 0 || !('transform' in staticValues)) {\n result.transform = validTransforms\n }\n }\n\n updateMapperState(mapperStateRef, emitted, ownedStatics)\n\n return result\n },\n isWeb\n ? [\n // pass SharedValues so the stable mapper watches them on web (no babel plugin)\n renderSnapshotRef,\n emitterSnapshotRef,\n configRef,\n isExitingRef,\n exitCycleIdShared,\n markExitKeyDone,\n isCompletingEnterRef,\n enterCycleIdShared,\n markEnterKeyDone,\n isCompletingUpdateRef,\n updateCycleIdShared,\n markUpdateKeyDone,\n fontSizeShared,\n leadingRatioShared,\n inheritedFontSize,\n ]\n : undefined\n )\n\n silenceAnimatedComponentDevCheck(animatedStyle)\n\n // Debug logging\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useAnimations', {\n resolvedTransition,\n componentState,\n isExiting,\n animatedStyles,\n staticStyles,\n baseConfig,\n propertyConfigs,\n })\n }\n\n // staticStyles carries animated keys only while mounting or on a key's\n // first post-mount appearance; after that the mapper owns them and the\n // commit bridge above keeps React commits from wiping its inline writes.\n return {\n style: [staticStyles, animatedStyle],\n // what descendants inherit: this node's mirror when it owns a font size\n // that moves, nothing when it owns one that does not (react-native\n // inherits that statically), otherwise what it inherited itself\n textChannel: publishesFontSize\n ? ownTextChannel\n : typeof style.fontSize === 'number'\n ? null\n : (inheritedText ?? null),\n }\n },\n }\n}\n"
|
|
10
10
|
]
|
|
11
11
|
}
|