@tamagui/animations-react-native 3.0.0-beta.1312.1 → 3.0.0-beta.1341.1

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.
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAAA,cAKO,wBAGA;AAGP,cAEE,oCAEA,yBACA,2BACA,8BACK;AAIP,SACE,eAIK,WACA,YACA;AAEP,cAAc,+BAA+B;AAwF7C,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAC7D,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAE7D,OAAO,iBAAS,kBACd,kBACC,wBAAwB,SAAS;KAgE/B,gBAAgB,wBAAwB,SAAS;AAEtD,OAAO,cAAM,2BAA2B,0BAA0B;AAgBlE,OAAO,cAAM,wBAAwB,uBAAuB;AAiC5D,OAAO,cAAM,0BACX,MAAM,iBACN,WAAW,GAAG;AAKhB,OAAO,iBAAS,iBAAiB,UAAU,kBACzC,YAAY,GACZ,UAAU,0BACT,mCAAmC",
2
+ "mappings": "AAAA,cAKO,wBAGA;AAGP,cAEE,oCAGA,yBACA,2BACA,8BACK;AAIP,SACE,eAKK,WACA,YACA;AAEP,cAAc,+BAA+B;AA2I7C,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAC7D,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAO7D,OAAO,iBAAS,kBACd,kBACC,wBAAwB,SAAS;KAgE/B,gBAAgB,wBAAwB,SAAS;AAEtD,OAAO,cAAM,2BAA2B,0BAA0B;AAgBlE,OAAO,cAAM,wBAAwB,uBAAuB;AAiC5D,OAAO,cAAM,0BACX,MAAM,iBACN,WAAW,GAAG;AAKhB,OAAO,iBAAS,iBAAiB,UAAU,kBACzC,YAAY,GACZ,UAAU,0BACT,mCAAmC",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/createAnimations.native.tsx"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "import {\n easingToBezier,\n forAnimationState,\n getTransitionForKey,\n resolveTransition,\n type AnimationsConfig,\n type ResolvedEntry,\n type ResolvedTransition,\n} from '@tamagui/animation-helpers'\nimport { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport type {\n AnimatedNumberStrategy,\n AnimationDriverWithAnimatedNumbers,\n TransitionProp,\n UniversalAnimatedNumber,\n UseAnimatedNumberReaction,\n UseAnimatedNumberStyle,\n} from '@tamagui/web'\nimport { useEvent } from '@tamagui/web'\nimport { useThemeWithState } from '@tamagui/web/internal-runtime'\nimport React from 'react'\nimport {\n Animated,\n Easing,\n processColor,\n type ColorValue,\n type Text,\n type View,\n} from 'react-native'\n\nimport type { CreateAnimationsOptions } from './types'\n\n// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys\nconst isFabric =\n !isWeb && typeof global !== 'undefined' && !!global.__nativeFabricUIManager\n\n// Helper to resolve dynamic theme values like {dynamic: {dark: \"value\", light: undefined}}\nconst resolveDynamicValue = (value: any, isDark: boolean): any => {\n if (value && typeof value === 'object' && 'dynamic' in value) {\n const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light\n return dynamicValue\n }\n return value\n}\n\n/** what `getAnimationConfig` hands to `Animated.spring` / `Animated.timing` */\ntype AnimationConfig =\n | ({ type: 'spring'; delay?: number } & Partial<\n Pick<\n Animated.SpringAnimationConfig,\n 'damping' | 'mass' | 'overshootClamping' | 'stiffness' | 'velocity'\n >\n >)\n | ({ type: 'timing'; delay?: number } & Partial<\n Pick<Animated.TimingAnimationConfig, 'duration' | 'easing'>\n >)\n\nconst animatedStyleKey = {\n transform: true,\n opacity: true,\n}\n\nconst colorStyleKey = {\n backgroundColor: true,\n color: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n}\n\n// layout dimension keys. these must run on the JS driver (useNativeDriver:false)\n// because the native animated module can't drive layout props.\nconst layoutStyleKey = {\n height: true,\n width: true,\n minHeight: true,\n maxHeight: true,\n minWidth: true,\n maxWidth: true,\n}\n\nfunction hasAnimatedLayoutKey(\n style: Record<string, any>,\n isDark: boolean,\n resolved: ResolvedTransition\n) {\n for (const key in layoutStyleKey) {\n if (!getTransitionForKey(resolved, key)) continue\n if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true\n }\n return false\n}\n\n// Only colors accepted by RN's own parser can enter interpolation. CSS-wide\n// keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach\n// createInterpolationFromStringOutputRange / mapStringToNumericComponents and\n// throw. Those values must be applied as static styles.\nfunction isAnimatableColor(value: unknown): value is string {\n return typeof value === 'string' && processColor(value as ColorValue) != null\n}\n\n// these style keys are costly to animate and only work with native driver on Fabric\nconst costlyToAnimateStyleKey = {\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n ...colorStyleKey,\n}\n\nexport const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View\nexport const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text\n\nexport function useAnimatedNumber(\n initial: number\n): UniversalAnimatedNumber<Animated.Value> {\n const state = React.useRef(\n null as any as {\n val: Animated.Value\n composite: Animated.CompositeAnimation | null\n strategy: AnimatedNumberStrategy\n }\n )\n if (!state.current) {\n state.current = {\n composite: null,\n val: new Animated.Value(initial),\n strategy: { type: 'spring' },\n }\n }\n\n return {\n getInstance() {\n return state.current.val\n },\n getValue() {\n return state.current.val['_value']\n },\n stop() {\n state.current.composite?.stop()\n state.current.composite = null\n },\n setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {\n const val = state.current.val\n\n const handleFinish = onFinish\n ? ({ finished }) => (finished ? onFinish() : null)\n : undefined\n\n if (type === 'direct') {\n state.current.composite?.stop()\n state.current.composite = null\n val.setValue(next)\n // a direct set finishes the moment it lands. not calling back stranded\n // everything waiting on it (sheet snap, presence completion).\n onFinish?.()\n } else if (type === 'spring') {\n state.current.composite?.stop()\n const composite = Animated.spring(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n } else {\n state.current.composite?.stop()\n const composite = Animated.timing(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n }\n },\n }\n}\n\ntype RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>\n\nexport const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (\n { value },\n onValue\n) => {\n const onChange = useEvent((current) => {\n onValue(current.value)\n })\n\n React.useEffect(() => {\n const id = value.getInstance().addListener(onChange)\n return () => {\n value.getInstance().removeListener(id)\n }\n }, [value, onChange])\n}\n\nexport const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (\n value,\n getStyle\n) => {\n const instance = value.getInstance()\n const animatedStyle = getStyle(instance)\n const usesAnimatedNode = hasAnimatedNode(animatedStyle)\n const [current, setCurrent] = React.useState(value.getValue())\n\n // preserve the native animated-node path for direct mappings. callbacks\n // that do arithmetic require numeric values, so drive those through the\n // value listener and render the computed style.\n React.useEffect(() => {\n if (usesAnimatedNode) return\n\n const id = instance.addListener(({ value: next }) => {\n setCurrent(next)\n })\n return () => {\n instance.removeListener(id)\n }\n }, [instance, usesAnimatedNode])\n\n return usesAnimatedNode ? animatedStyle : getStyle(current)\n}\n\nfunction hasAnimatedNode(value: unknown): boolean {\n if (!value || typeof value !== 'object') return false\n if (typeof (value as any).__getValue === 'function') return true\n if (Array.isArray(value)) return value.some(hasAnimatedNode)\n return Object.values(value).some(hasAnimatedNode)\n}\n\nexport const useAnimatedNumbersStyle = (\n vals: RNAnimatedNum[],\n getStyle: (...currentValues: any[]) => any\n): any => {\n return getStyle(...vals.map((v) => v.getInstance()))\n}\n\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A,\n options?: CreateAnimationsOptions\n): AnimationDriverWithAnimatedNumbers<A> {\n const nativeDriver = options?.useNativeDriver ?? isFabric\n\n return {\n inputStyle: 'value',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n needsCustomComponent: true,\n View: AnimatedView,\n Text: AnimatedText,\n useAnimatedNumber,\n useAnimatedNumberReaction,\n useAnimatedNumberStyle,\n useAnimatedNumbersStyle,\n usePresence,\n ResetPresence,\n useAnimations: ({\n props,\n onTransition,\n style,\n componentState,\n presence,\n stateRef,\n styleState,\n useStyleEmitter,\n }) => {\n const isDisabled = isWeb && componentState.unmounted === true\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n const onTransitionRef = React.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 // createComponent merges a colocated `transition` out of the active\n // pseudo style (`enterStyle={{ opacity: 0, transition: '200ms' }}`), so\n // this is the one that applies right now, not the base prop.\n const effectiveTransition = (styleState?.effectiveTransition ?? props.transition) as\n | TransitionProp\n | null\n | undefined\n\n const [, themeState] = useThemeWithState({})\n // Check scheme first, then fall back to checking theme name for 'dark'\n const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')\n\n /** store Animated value of each key e.g: color: AnimatedValue */\n const animateStyles = React.useRef<Record<string, Animated.Value>>({})\n const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])\n const animationsState = React.useRef(\n new WeakMap<\n Animated.Value,\n {\n interpolation: Animated.AnimatedInterpolation<any>\n current?: number | string | undefined\n // only for colors\n animateToValue?: number\n }\n >()\n )\n const pseudoActiveRef = React.useRef(false)\n\n // exit cycle guards to prevent stale/duplicate completion\n const exitCycleIdRef = React.useRef(0)\n const exitCompletedRef = React.useRef(false)\n const wasExitingRef = React.useRef(false)\n\n // onTransition lifecycle bookkeeping\n const enterStartedRef = React.useRef(false)\n const exitStartedRef = React.useRef(false)\n const updateInFlightRef = React.useRef(false)\n const updateCycleIdRef = React.useRef(0)\n const prevStyleSigRef = React.useRef<string | null>(null)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // start new exit cycle only on transition INTO exiting\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n }\n\n // Track if we just finished entering (transition from entering to not entering)\n // must be declared before args array that uses justFinishedEntering\n const isEntering = !!componentState.unmounted\n const wasEnteringRef = React.useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n const args = [\n JSON.stringify(style),\n JSON.stringify(effectiveTransition),\n componentState,\n isExiting,\n !!onTransition,\n isDark,\n justFinishedEntering,\n ]\n\n const res = React.useMemo(() => {\n const runners: Function[] = []\n const completions: Promise<void>[] = []\n\n // Determine animation state for enter/exit transitions\n // Use 'enter' if we're entering OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'default'\n\n // which style keys animate at all is the transition's own decision, so\n // a property list narrows this the same way it narrows css\n const resolved = forAnimationState(\n resolveTransition(effectiveTransition, { animations }),\n animationState\n )\n\n const nonAnimatedStyle = {}\n // animatedStyle owns every Animated.Value on the node. Fabric cannot mix\n // native- and JS-driven values inside that shared graph, so one layout\n // animation makes the whole node use the JS driver.\n const useNativeDriverForNode =\n nativeDriver && !hasAnimatedLayoutKey(style, isDark, resolved)\n\n // track which animated keys/transforms the incoming style actually\n // carries this pass, so entries that left the style can be dropped\n // below (an Animated.Value that persisted forever would keep painting\n // a stale pixel value, e.g. a released-to-auto accordion height)\n const seenAnimateKeys = new Set<string>()\n let sawTransform = false\n let transformCount = 0\n\n for (const key in style) {\n const rawVal = style[key]\n // Resolve dynamic theme values from flat theme clauses.\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (isDisabled) {\n continue\n }\n\n if (\n animatedStyleKey[key] == null &&\n !costlyToAnimateStyleKey[key] &&\n !layoutStyleKey[key]\n ) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // `transform` is a container, not a property: its parts each resolve\n // on their own below, and a part no entry covers gets `snapConfig`.\n // the array cannot be split into animated and static halves here.\n if (key !== 'transform' && !getTransitionForKey(resolved, key)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // layout dimension keys only animate numbers — 'auto' (an open\n // accordion at rest) and percent strings apply as static styles\n if (layoutStyleKey[key] && typeof val !== 'number') {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // unparseable colors crash RN\n // interpolation — apply them as a static style instead\n if (colorStyleKey[key] && !isAnimatableColor(val)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (key !== 'transform') {\n animateStyles.current[key] = update(key, animateStyles.current[key], val)\n seenAnimateKeys.add(key)\n continue\n }\n // key: 'transform'\n // for now just support one transform key\n if (!val) continue\n if (typeof val === 'string') {\n console.warn(`Warning: Tamagui can't animate string transforms yet!`)\n continue\n }\n\n sawTransform = true\n for (const transform of val) {\n if (!transform) continue\n const index = transformCount++\n // tkey: e.g: 'translateX'\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n animatedTranforms.current = [...animatedTranforms.current]\n }\n }\n\n // drop stale Animated.Values whose keys left the incoming style, so the\n // key genuinely leaves the rendered style object (a released height goes\n // back to auto instead of staying pinned at its last pixel value). skip\n // while exiting (presence still animates the leaving keys) and while\n // disabled (the loop above intentionally skips every key). an active\n // pseudo owns the current emitted style until its matching release.\n if (!isExiting && !isDisabled && !pseudoActiveRef.current) {\n for (const k in animateStyles.current) {\n if (!seenAnimateKeys.has(k)) delete animateStyles.current[k]\n }\n if (!sawTransform) {\n if (animatedTranforms.current.length) animatedTranforms.current = []\n } else if (animatedTranforms.current.length > transformCount) {\n animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)\n }\n }\n\n const animatedTransformStyle =\n animatedTranforms.current.length > 0\n ? {\n transform: animatedTranforms.current.map((r) => {\n const key = Object.keys(r)[0]\n const val =\n animationsState.current!.get(r[key])?.interpolation || r[key]\n return { [key]: val }\n }),\n }\n : {}\n\n const animatedStyle = {\n ...Object.fromEntries(\n Object.entries(animateStyles.current).map(([k, v]) => [\n k,\n animationsState.current!.get(v)?.interpolation || v,\n ])\n ),\n ...animatedTransformStyle,\n }\n\n return {\n runners,\n completions,\n style: [nonAnimatedStyle, animatedStyle],\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColorStyleKey = colorStyleKey[key]\n const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)\n let animateToValue = val\n const value = animated || new Animated.Value(val)\n const curInterpolation = animationsState.current.get(value)\n\n let interpolateArgs: any\n if (type) {\n interpolateArgs = getInterpolated(\n curInterpolation?.current ?? value['_value'],\n val,\n type\n )\n animationsState.current!.set(value, {\n interpolation: value.interpolate(interpolateArgs),\n current: val,\n })\n }\n\n if (isColorStyleKey) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n interpolateArgs = getColorInterpolated(\n curInterpolation?.current as string,\n // valIn is the next color\n valIn as string,\n animateToValue\n )\n animationsState.current!.set(value, {\n current: valIn,\n interpolation: value.interpolate(interpolateArgs),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n if (value) {\n const animationConfig = getAnimationConfig(\n key,\n animations,\n effectiveTransition,\n animationState\n )\n\n let resolve\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n completions.push(promise)\n\n runners.push(() => {\n value.stopAnimation()\n\n // `delay` drives the sequence below, so it must not also ride\n // along in the config or every delayed animation waits twice\n const { type, delay, ...config } = animationConfig\n const animation = Animated[type || 'spring'](value, {\n toValue: animateToValue,\n ...config,\n useNativeDriver: useNativeDriverForNode,\n })\n const animation2 = delay\n ? Animated.sequence([Animated.delay(delay), animation])\n : animation\n\n animation2.start(({ finished }) => {\n // always resolve during exit (element is leaving anyway)\n // for non-exit, only resolve on successful completion\n if (finished || isExiting) {\n resolve()\n }\n })\n })\n }\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n // prettier-ignore\n console.info(\n ' 💠 animate',\n key,\n `from (${value['_value']}) to`,\n valIn,\n `(${val})`,\n 'type',\n type,\n 'interpolate',\n interpolateArgs\n )\n }\n }\n return value\n }\n }, args)\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\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 useIsomorphicLayoutEffect(() => {\n res.runners.forEach((r) => r())\n\n // capture current cycle id\n const cycleId = exitCycleIdRef.current\n\n const cause: 'enter' | 'exit' | 'update' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'update'\n\n // interruptions: an enter or update still in flight when exit begins is\n // reported as finished:false (its own completion promise won't resolve\n // because the animation was stopped, not finished).\n if (cause === 'exit') {\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', false)\n }\n if (updateInFlightRef.current) {\n updateInFlightRef.current = false\n updateCycleIdRef.current++\n emit('end', 'update', false)\n }\n }\n\n // in-place update: a genuine style change while mounted (not entering or\n // exiting). guard on the style signature so lifecycle-only re-renders\n // don't register as updates.\n if (cause === 'update') {\n const sig = args[0] as string\n if (prevStyleSigRef.current === null || prevStyleSigRef.current === sig) {\n prevStyleSigRef.current = sig\n return\n }\n prevStyleSigRef.current = sig\n if (res.completions.length === 0) return\n if (updateInFlightRef.current) {\n // superseded before finishing\n emit('end', 'update', false)\n }\n updateInFlightRef.current = true\n const uid = ++updateCycleIdRef.current\n emit('start', 'update')\n Promise.all(res.completions).then(() => {\n if (uid !== updateCycleIdRef.current) return\n updateInFlightRef.current = false\n emit('end', 'update', true)\n })\n return\n }\n\n // keep the update signature current while entering/exiting\n prevStyleSigRef.current = args[0] as string\n\n // handle zero-completion case immediately (enter/exit report a pair)\n if (res.completions.length === 0) {\n emit('start', cause)\n emit('end', cause, true)\n if (isExiting && !exitCompletedRef.current) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n return\n }\n\n // enter/exit start (once per cycle; re-runs continue the same animation)\n if (cause === 'enter' && !enterStartedRef.current) {\n enterStartedRef.current = true\n emit('start', 'enter')\n }\n if (cause === 'exit' && !exitStartedRef.current) {\n exitStartedRef.current = true\n emit('start', 'exit')\n }\n\n Promise.all(res.completions).then(() => {\n // guard against stale cycle completion\n if (isExiting && cycleId !== exitCycleIdRef.current) return\n if (isExiting && exitCompletedRef.current) return\n\n if (isExiting) {\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n // exit 'end' fires immediately before presence safeToRemove\n emit('end', 'exit', true)\n }\n exitCompletedRef.current = true\n sendExitComplete?.()\n } else if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n })\n }, args)\n\n // avoidReRenders: receive style changes imperatively from tamagui\n // and update Animated.Values directly without React re-renders\n // reuses the same update() + runner pattern as the useMemo path\n useStyleEmitter?.((nextStyle, emittedTransition, pseudoActive) => {\n pseudoActiveRef.current = pseudoActive === true\n const runners: Function[] = []\n const seenAnimateKeys = new Set<string>()\n let transformCount = 0\n let animatedShapeChanged = false\n // the emitter runs on a mounted node, so `default` is the state, but\n // the transition is the one it was handed\n const emittedResolved = forAnimationState(\n resolveTransition(emittedTransition ?? effectiveTransition, { animations }),\n 'default'\n )\n // nextStyle is the complete style for this node, so the emitter makes\n // the same single driver decision as the render path. include the\n // currently rendered graph because its stale keys are not removed until\n // the structural-change commit below.\n const useNativeDriverForNode =\n nativeDriver &&\n !hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&\n !Object.keys(animateStyles.current).some((key) => layoutStyleKey[key])\n\n for (const key in nextStyle) {\n const rawVal = nextStyle[key]\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (key === 'transform' && Array.isArray(val)) {\n for (const transform of val) {\n if (!transform) continue\n const index = transformCount++\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n if (!currentTransform) animatedShapeChanged = true\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n }\n } else if (\n animatedStyleKey[key] != null ||\n costlyToAnimateStyleKey[key] ||\n layoutStyleKey[key]\n ) {\n // layout keys only animate numbers ('auto'/percents are static);\n // unparseable themed colors can't be interpolated — skip both and\n // let the next render apply them statically\n if (layoutStyleKey[key] && typeof val !== 'number') continue\n if (colorStyleKey[key] && !isAnimatableColor(val)) continue\n if (!animateStyles.current[key]) animatedShapeChanged = true\n animateStyles.current[key] = update(key, animateStyles.current[key], val)\n seenAnimateKeys.add(key)\n }\n }\n\n // the emitter receives a complete style. keep the Animated style graph\n // equally complete, including a pseudo release that omits a pseudo-only\n // key. React Native needs a commit when that graph's shape changes.\n for (const key in animateStyles.current) {\n if (!seenAnimateKeys.has(key)) {\n delete animateStyles.current[key]\n animatedShapeChanged = true\n }\n }\n if (animatedTranforms.current.length > transformCount) {\n animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)\n animatedShapeChanged = true\n }\n\n // run the queued animations immediately\n runners.forEach((r) => r())\n\n // pseudo state normally stays on the avoidReRenders path. adding or\n // removing a style key cannot be expressed by an existing Animated.Value,\n // so commit the pending state only for that structural change.\n if (animatedShapeChanged && stateRef.current.nextState) {\n stateRef.current.baseSetStateShallow?.(stateRef.current.nextState)\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColor = colorStyleKey[key]\n const [numVal, type] = isColor ? [0, undefined] : getValue(valIn)\n let animateToValue = numVal\n const value = animated || new Animated.Value(numVal)\n const curInterpolation = animationsState.current.get(value)\n\n if (type) {\n animationsState.current.set(value, {\n interpolation: value.interpolate(\n getInterpolated(\n curInterpolation?.current ?? value['_value'],\n numVal,\n type\n )\n ),\n current: numVal,\n })\n }\n\n if (isColor) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n animationsState.current.set(value, {\n current: valIn,\n interpolation: value.interpolate(\n getColorInterpolated(\n curInterpolation?.current as string,\n valIn as string,\n animateToValue\n )\n ),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n // the emitter runs for pseudo-state changes on a mounted node, so\n // `default` is the state, but the transition is the one it was handed\n const animationConfig = getAnimationConfig(\n key,\n animations,\n emittedTransition ?? effectiveTransition,\n 'default'\n )\n runners.push(() => {\n value.stopAnimation()\n const { type, delay, ...config } = animationConfig\n const anim = Animated[type || 'spring'](value, {\n toValue: animateToValue,\n ...config,\n useNativeDriver: useNativeDriverForNode,\n })\n ;(delay ? Animated.sequence([Animated.delay(delay), anim]) : anim).start()\n })\n\n return value\n }\n })\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n console.info(`Animated`, { response: res, inputStyle: style, isExiting })\n }\n }\n\n return res\n },\n }\n}\n\nfunction getColorInterpolated(\n currentColor: string | undefined,\n nextColor: string,\n animateToValue: number\n) {\n const inputRange = [0, 1]\n const outputRange = [currentColor ? currentColor : nextColor, nextColor]\n if (animateToValue === 0) {\n // because we are animating from value 1 to 0, we need to put target color at the beginning\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getInterpolated(current: number, next: number, postfix = 'deg') {\n if (next === current) {\n current = next - 0.000000001\n }\n const inputRange = [current, next]\n const outputRange = [`${current}${postfix}`, `${next}${postfix}`]\n if (next < current) {\n inputRange.reverse()\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\n/**\n * one resolved entry as a react-native Animated config.\n *\n * springs go in as stiffness/damping/mass, which is the parameterization RN\n * actually integrates. `bounciness`/`speed` and `tension`/`friction` are older\n * spellings of the same two numbers, so nothing is lost by not using them.\n */\nfunction entryToRN(entry: ResolvedEntry): AnimationConfig {\n const extra = entry.timing.kind === 'spring' ? entry.timing.extra : undefined\n\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 ...(typeof extra?.velocity === 'number' ? { velocity: extra.velocity } : null),\n ...(typeof extra?.overshootClamping === 'boolean'\n ? { overshootClamping: extra.overshootClamping }\n : null),\n ...(entry.delayMs ? { delay: entry.delayMs } : null),\n }\n }\n\n const bezier = easingToBezier(entry.timing.easing)\n return {\n type: 'timing',\n duration: entry.timing.durationMs,\n // `linear()` and `steps()` have no bezier equivalent; RN's default easing\n // is the honest answer rather than a curve we made up\n ...(bezier\n ? { easing: Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) }\n : null),\n ...(entry.delayMs ? { delay: entry.delayMs } : null),\n }\n}\n\n// a key the transition does not cover does not animate. snapping is what css\n// does for an unlisted property, so the drivers have to agree on it too.\nconst snapConfig: AnimationConfig = { type: 'timing', duration: 0 }\n\nfunction getAnimationConfig(\n key: string,\n animations: AnimationsConfig,\n transition?: TransitionProp | null,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n): AnimationConfig {\n const resolved = forAnimationState(\n resolveTransition(transition, { animations }),\n animationState\n )\n const entry = getTransitionForKey(resolved, key)\n return entry ? entryToRN(entry) : snapConfig\n}\n\nfunction getValue(input: number | string, isColor = false) {\n if (typeof input !== 'string') {\n return [input] as const\n }\n // the unit is optional: unitless numbers reach here as strings (scale, and\n // any bare token value), and the number may be fractional. matching only\n // `[-0-9]+` followed by a required unit read \"1.5deg\" as 5 and gave NaN for\n // \"0.95\", and an Animated animation toward NaN never calls its completion\n // callback, which strands whatever waits on it.\n const [_, number, after] = input.match(/(-?(?:\\d+\\.?\\d*|\\.\\d+))(deg|%|px)?/) ?? []\n return [+number, after] as const\n}\n"
9
+ "import {\n easingToBezier,\n forAnimationState,\n getTransitionForKey,\n resolveTransition,\n type AnimationsConfig,\n type ResolvedEntry,\n type ResolvedTransition,\n} from '@tamagui/animation-helpers'\nimport { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport type {\n AnimatedNumberStrategy,\n AnimationDriverWithAnimatedNumbers,\n NativeTextMetrics,\n TransitionProp,\n UniversalAnimatedNumber,\n UseAnimatedNumberReaction,\n UseAnimatedNumberStyle,\n} from '@tamagui/web'\nimport { useEvent } from '@tamagui/web'\nimport { useThemeWithState } from '@tamagui/web/internal-runtime'\nimport React from 'react'\nimport {\n Animated,\n Easing,\n processColor,\n TextInput,\n type ColorValue,\n type Text,\n type View,\n} from 'react-native'\n\nimport type { CreateAnimationsOptions } from './types'\n\n// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys\nconst isFabric =\n !isWeb && typeof global !== 'undefined' && !!global.__nativeFabricUIManager\n\n// Helper to resolve dynamic theme values like {dynamic: {dark: \"value\", light: undefined}}\nconst resolveDynamicValue = (value: any, isDark: boolean): any => {\n if (value && typeof value === 'object' && 'dynamic' in value) {\n const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light\n return dynamicValue\n }\n return value\n}\n\n/** what `getAnimationConfig` hands to `Animated.spring` / `Animated.timing` */\ntype AnimationConfig =\n | ({ type: 'spring'; delay?: number } & Partial<\n Pick<\n Animated.SpringAnimationConfig,\n 'damping' | 'mass' | 'overshootClamping' | 'stiffness' | 'velocity'\n >\n >)\n | ({ type: 'timing'; delay?: number } & Partial<\n Pick<Animated.TimingAnimationConfig, 'duration' | 'easing'>\n >)\n\nconst animatedStyleKey = {\n transform: true,\n opacity: true,\n}\n\nconst colorStyleKey = {\n backgroundColor: true,\n color: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n}\n\n// layout dimensions. the native animated module has no whitelist entry for any\n// of them, so a node animating one runs its whole Animated graph on the JS\n// driver (useNativeDriver:false): Fabric cannot mix native- and JS-driven\n// values on one node.\nconst layoutStyleKey = {\n height: true,\n width: true,\n minHeight: true,\n maxHeight: true,\n minWidth: true,\n maxWidth: true,\n}\n\n// text metrics have no whitelist entry either, but unlike a height they sit on\n// nearly every Text, so being covered by a broad `transition=\"medium\"` must not\n// be enough to take the node off the native driver: an opacity fade over text\n// whose size is not changing would then run in JS. they join the Animated graph\n// only while they are actually moving, and the node's driver follows them.\nconst textMetricStyleKey = {\n fontSize: true,\n lineHeight: true,\n}\n\nconst jsDriverStyleKey = { ...layoutStyleKey, ...textMetricStyleKey }\n\nfunction hasAnimatedLayoutKey(\n style: Record<string, any>,\n isDark: boolean,\n resolved: ResolvedTransition\n) {\n for (const key in layoutStyleKey) {\n if (!getTransitionForKey(resolved, key)) continue\n if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true\n }\n return false\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. This driver\n// gets that exactly by never animating the leading itself: it animates the\n// font size and the ratio, and paints their product as one Animated node.\n// An absolute px leading, and any raw react-native style, keeps its own value\n// and animates as an ordinary numeric key.\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 product needs both factors as numbers in the style it is painting into.\nconst paintsLeadingProduct = (\n style: Record<string, any>,\n ratio: number | undefined\n): ratio is number =>\n ratio !== undefined &&\n typeof style.fontSize === 'number' &&\n typeof style.lineHeight === 'number'\n\n// a leading multiplied off an ANCESTOR's live font size: this node has none of\n// its own, core reports the ratio, and react-native inherits the size itself.\nconst inheritedFontSizeNode = (\n style: Record<string, any>,\n ratio: number | undefined,\n inheritedText: { fontSize: unknown; driver: string } | null | undefined\n) =>\n ratio !== undefined &&\n typeof style.fontSize !== 'number' &&\n inheritedText?.driver === 'react-native'\n ? (inheritedText.fontSize as Animated.Value)\n : undefined\n\n// Only colors accepted by RN's own parser can enter interpolation. CSS-wide\n// keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach\n// createInterpolationFromStringOutputRange / mapStringToNumericComponents and\n// throw. Those values must be applied as static styles.\nfunction isAnimatableColor(value: unknown): value is string {\n return typeof value === 'string' && processColor(value as ColorValue) != null\n}\n\n// these style keys are costly to animate and only work with native driver on Fabric\nconst costlyToAnimateStyleKey = {\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n ...colorStyleKey,\n}\n\nexport const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View\nexport const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text\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 nodes of its own.\n// built on first use: the compiler evaluates tamagui.config.ts against a\n// react-native stub whose Animated cannot make one, and never binds any text.\nlet animatedTextInput: Animated.AnimatedComponent<typeof TextInput> | undefined\n\nexport function useAnimatedNumber(\n initial: number\n): UniversalAnimatedNumber<Animated.Value> {\n const state = React.useRef(\n null as any as {\n val: Animated.Value\n composite: Animated.CompositeAnimation | null\n strategy: AnimatedNumberStrategy\n }\n )\n if (!state.current) {\n state.current = {\n composite: null,\n val: new Animated.Value(initial),\n strategy: { type: 'spring' },\n }\n }\n\n return {\n getInstance() {\n return state.current.val\n },\n getValue() {\n return state.current.val['_value']\n },\n stop() {\n state.current.composite?.stop()\n state.current.composite = null\n },\n setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {\n const val = state.current.val\n\n const handleFinish = onFinish\n ? ({ finished }) => (finished ? onFinish() : null)\n : undefined\n\n if (type === 'direct') {\n state.current.composite?.stop()\n state.current.composite = null\n val.setValue(next)\n // a direct set finishes the moment it lands. not calling back stranded\n // everything waiting on it (sheet snap, presence completion).\n onFinish?.()\n } else if (type === 'spring') {\n state.current.composite?.stop()\n const composite = Animated.spring(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n } else {\n state.current.composite?.stop()\n const composite = Animated.timing(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n }\n },\n }\n}\n\ntype RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>\n\nexport const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (\n { value },\n onValue\n) => {\n const onChange = useEvent((current) => {\n onValue(current.value)\n })\n\n React.useEffect(() => {\n const id = value.getInstance().addListener(onChange)\n return () => {\n value.getInstance().removeListener(id)\n }\n }, [value, onChange])\n}\n\nexport const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (\n value,\n getStyle\n) => {\n const instance = value.getInstance()\n const animatedStyle = getStyle(instance)\n const usesAnimatedNode = hasAnimatedNode(animatedStyle)\n const [current, setCurrent] = React.useState(value.getValue())\n\n // preserve the native animated-node path for direct mappings. callbacks\n // that do arithmetic require numeric values, so drive those through the\n // value listener and render the computed style.\n React.useEffect(() => {\n if (usesAnimatedNode) return\n\n const id = instance.addListener(({ value: next }) => {\n setCurrent(next)\n })\n return () => {\n instance.removeListener(id)\n }\n }, [instance, usesAnimatedNode])\n\n return usesAnimatedNode ? animatedStyle : getStyle(current)\n}\n\nfunction hasAnimatedNode(value: unknown): boolean {\n if (!value || typeof value !== 'object') return false\n if (typeof (value as any).__getValue === 'function') return true\n if (Array.isArray(value)) return value.some(hasAnimatedNode)\n return Object.values(value).some(hasAnimatedNode)\n}\n\nexport const useAnimatedNumbersStyle = (\n vals: RNAnimatedNum[],\n getStyle: (...currentValues: any[]) => any\n): any => {\n return getStyle(...vals.map((v) => v.getInstance()))\n}\n\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A,\n options?: CreateAnimationsOptions\n): AnimationDriverWithAnimatedNumbers<A> {\n const nativeDriver = options?.useNativeDriver ?? isFabric\n\n return {\n inputStyle: 'value',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n needsCustomComponent: true,\n View: AnimatedView,\n Text: AnimatedText,\n useAnimatedNumber,\n useAnimatedNumberReaction,\n useAnimatedNumberStyle,\n useAnimatedNumbersStyle,\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 font size node is the ancestor's, and a\n // ratio leading is a multiplication of it, so both land on the same frame.\n useTextMetrics: ({ inheritedText, lineHeight }) => {\n const node =\n inheritedText?.driver === 'react-native'\n ? (inheritedText.fontSize as Animated.Value)\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 style = React.useMemo(\n () =>\n node\n ? ratio === null\n ? { fontSize: node }\n : { fontSize: node, lineHeight: Animated.multiply(node, ratio) }\n : null,\n [node, ratio]\n )\n const textChannel = inheritedText ?? null\n if (!style) return { style: null, textChannel }\n animatedTextInput ||= Animated.createAnimatedComponent(TextInput)\n return { style, textChannel, Text: AnimatedText, TextInput: animatedTextInput }\n },\n\n useAnimations: ({\n props,\n onTransition,\n style,\n componentState,\n presence,\n stateRef,\n styleState,\n useStyleEmitter,\n inheritedText,\n }) => {\n const isDisabled = isWeb && componentState.unmounted === true\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n const onTransitionRef = React.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 // createComponent merges a colocated `transition` out of the active\n // pseudo style (`enterStyle={{ opacity: 0, transition: '200ms' }}`), so\n // this is the one that applies right now, not the base prop.\n const effectiveTransition = (styleState?.effectiveTransition ?? props.transition) as\n | TransitionProp\n | null\n | undefined\n\n const [, themeState] = useThemeWithState({})\n // Check scheme first, then fall back to checking theme name for 'dark'\n const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')\n\n /** store Animated value of each key e.g: color: AnimatedValue */\n const animateStyles = React.useRef<Record<string, Animated.Value>>({})\n // a ratio leading animates the RATIO, never the leading, and paints\n // fontSize * ratio as one node. the ratio is not a style key, so it lives\n // outside animateStyles (which the stale-key sweep and the emitter's\n // structural-change check both treat as the rendered style's own keys).\n const leadingRatioValue = React.useRef<Animated.Value | null>(null)\n const leadingRatioRef = React.useRef<number | undefined>(undefined)\n // whether the style React last committed paints the derived product. the\n // emitter can only re-target existing Animated.Values, so a pass that\n // disagrees with this has to force a commit to swap the node itself.\n const paintsDerivedLeading = React.useRef(false)\n // this subtree's live font size, published for descendants that inherit\n // it. the channel's identity IS the node's, so a consumer that keeps the\n // same channel keeps the same derived node.\n const textChannelRef = React.useRef<{ fontSize: unknown; driver: string } | null>(\n null\n )\n // core reports a numeric (ratio) leading on nativeTextMetrics; a px length\n // and 'normal' are reported as themselves and stay independent keys.\n leadingRatioRef.current = getLeadingRatio(styleState?.nativeTextMetrics)\n // the text metrics the last pass painted, so the next one can tell a real\n // change from a value that is simply present. a metric only joins the\n // Animated graph while it is moving (see textMetricStyleKey), which is\n // what keeps an ordinary opacity or transform animation over unchanged\n // text on the native driver.\n const paintedTextRef = React.useRef<{\n fontSize?: unknown\n lineHeight?: unknown\n ratio?: number\n }>({})\n // a value already in the graph keeps moving until it arrives, so a\n // re-render mid-flight never drops it back to a static style and snaps.\n // the first pass has painted nothing yet, so nothing is moving there\n // either: the metrics land as plain numbers and the node mounts on the\n // native driver.\n const movesTextValue = (key: 'fontSize' | 'lineHeight', target: unknown) => {\n if (typeof target !== 'number') return false\n const node = animateStyles.current[key]\n const last = node ? (node['_value'] as unknown) : paintedTextRef.current[key]\n return last !== undefined && last !== target\n }\n const movesLeadingRatio = (ratio: number | undefined) => {\n if (ratio === undefined) return false\n const node = leadingRatioValue.current\n const last = node ? (node['_value'] as number) : paintedTextRef.current.ratio\n return last !== undefined && last !== ratio\n }\n // the font size descendants read as this node's live size. it outlives\n // any one animation: dropping it whenever the size came to rest would\n // swap every descendant between Text and Animated.Text, remounting their\n // subtrees, and a TextInput's focus and contents with them, every time an\n // animation started or finished.\n const fontSizeNode = React.useRef<Animated.Value | null>(null)\n const animatedValueFor = (key: string) => {\n const painted = animateStyles.current[key]\n if (painted) return painted\n if (key === 'fontSize' && fontSizeNode.current) return fontSizeNode.current\n // a metric that was at rest is out of the graph, so the value it\n // rejoins with has to start at what the last pass painted: a fresh\n // Animated.Value starts AT its target, and would snap the metric.\n if (textMetricStyleKey[key])\n return new Animated.Value(paintedTextRef.current[key] as number)\n return undefined\n }\n const leadingRatioValueFrom = () =>\n leadingRatioValue.current ??\n new Animated.Value(paintedTextRef.current.ratio as number)\n const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])\n const animationsState = React.useRef(\n new WeakMap<\n Animated.Value,\n {\n interpolation: Animated.AnimatedInterpolation<any>\n current?: number | string | undefined\n // only for colors\n animateToValue?: number\n }\n >()\n )\n const pseudoActiveRef = React.useRef(false)\n\n // exit cycle guards to prevent stale/duplicate completion\n const exitCycleIdRef = React.useRef(0)\n const exitCompletedRef = React.useRef(false)\n const wasExitingRef = React.useRef(false)\n\n // onTransition lifecycle bookkeeping\n const enterStartedRef = React.useRef(false)\n const exitStartedRef = React.useRef(false)\n const updateInFlightRef = React.useRef(false)\n const updateCycleIdRef = React.useRef(0)\n const prevStyleSigRef = React.useRef<string | null>(null)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // start new exit cycle only on transition INTO exiting\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n }\n\n // Track if we just finished entering (transition from entering to not entering)\n // must be declared before args array that uses justFinishedEntering\n const isEntering = !!componentState.unmounted\n const wasEnteringRef = React.useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n const args = [\n JSON.stringify(style),\n JSON.stringify(effectiveTransition),\n componentState,\n isExiting,\n !!onTransition,\n isDark,\n justFinishedEntering,\n leadingRatioRef.current,\n inheritedText,\n ]\n\n const res = React.useMemo(() => {\n const runners: Function[] = []\n const completions: Promise<void>[] = []\n\n // Determine animation state for enter/exit transitions\n // Use 'enter' if we're entering OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'default'\n\n // which style keys animate at all is the transition's own decision, so\n // a property list narrows this the same way it narrows css\n const resolved = forAnimationState(\n resolveTransition(effectiveTransition, { animations }),\n animationState\n )\n\n const nonAnimatedStyle = {}\n // the leading is derived only while something can actually move it.\n // when neither key is covered by the transition both fall through to\n // nonAnimatedStyle and land together in one commit, which is already\n // coherent and keeps the node on the native driver.\n const paintedRatio = leadingRatioRef.current\n const paintsProduct = paintsLeadingProduct(style, paintedRatio)\n const movesFontSize = movesTextValue('fontSize', style.fontSize)\n const movesLeading = paintsProduct\n ? movesLeadingRatio(paintedRatio)\n : movesTextValue('lineHeight', style.lineHeight)\n // a metric that has arrived leaves the graph here rather than in the\n // sweep at the end of the pass, so the node goes back to the native\n // driver in the same commit that stops moving it. the same three\n // conditions as that sweep: an exit and a latched pseudo both still own\n // the keys they are painting.\n if (!isExiting && !isDisabled && !pseudoActiveRef.current) {\n if (!movesFontSize) delete animateStyles.current.fontSize\n if (!movesLeading) delete animateStyles.current.lineHeight\n }\n\n // a font size a transition can move is published to descendants whether\n // or not it happens to be moving right now, so their host component\n // does not change under them when it starts.\n if (\n isDisabled ||\n typeof style.fontSize !== 'number' ||\n !getTransitionForKey(resolved, 'fontSize')\n ) {\n fontSizeNode.current = null\n } else if (!fontSizeNode.current) {\n fontSizeNode.current = new Animated.Value(style.fontSize)\n }\n\n // track which animated keys/transforms the incoming style actually\n // carries this pass, so entries that left the style can be dropped\n // below (an Animated.Value that persisted forever would keep painting\n // a stale pixel value, e.g. a released-to-auto accordion height)\n const seenAnimateKeys = new Set<string>()\n let sawTransform = false\n let transformCount = 0\n\n const derivesLeading =\n !isDisabled &&\n paintsProduct &&\n (movesFontSize || movesLeading) &&\n (!!getTransitionForKey(resolved, 'fontSize') ||\n !!getTransitionForKey(resolved, 'lineHeight'))\n // an inherited font size: core leaves this node's own fontSize out of\n // the style and reports the ratio, so the leading is the ANCESTOR's\n // live font size times this node's ratio. react-native inherits the\n // size itself, and a length leading is inherited as a length.\n const inheritedFontSize = isDisabled\n ? undefined\n : inheritedFontSizeNode(style, paintedRatio, inheritedText)\n\n // animatedStyle owns every Animated.Value on the node. Fabric cannot mix\n // native- and JS-driven values inside that shared graph, so one layout\n // animation, one text metric on the move, or a leading multiplied off an\n // ancestor's font size, makes the whole node use the JS driver. a metric\n // another pass left in the graph counts too, or its next animation would\n // start on a driver the node no longer runs on.\n const useNativeDriverForNode =\n nativeDriver &&\n !hasAnimatedLayoutKey(style, isDark, resolved) &&\n !movesFontSize &&\n !movesLeading &&\n !inheritedFontSize &&\n !Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])\n\n for (const key in style) {\n const rawVal = style[key]\n // Resolve dynamic theme values from flat theme clauses.\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (isDisabled) {\n continue\n }\n\n // fontSize and lineHeight are owned by the derived-leading block below\n if (\n (derivesLeading || inheritedFontSize) &&\n (key === 'fontSize' || key === 'lineHeight')\n ) {\n // unless the size is the factor at rest: the product multiplies the\n // number it paints, and that number has to be painted\n if (key === 'fontSize' && derivesLeading && !movesFontSize) {\n nonAnimatedStyle[key] = val\n }\n continue\n }\n\n // a text metric at rest stays out of the Animated graph entirely: it\n // paints as a plain style, and the node keeps the native driver\n if (\n textMetricStyleKey[key] &&\n !(key === 'fontSize' ? movesFontSize : movesLeading)\n ) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (\n animatedStyleKey[key] == null &&\n !costlyToAnimateStyleKey[key] &&\n !jsDriverStyleKey[key]\n ) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // `transform` is a container, not a property: its parts each resolve\n // on their own below, and a part no entry covers gets `snapConfig`.\n // the array cannot be split into animated and static halves here.\n if (key !== 'transform' && !getTransitionForKey(resolved, key)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // layout dimension keys only animate numbers — 'auto' (an open\n // accordion at rest) and percent strings apply as static styles\n if (jsDriverStyleKey[key] && typeof val !== 'number') {\n nonAnimatedStyle[key] = val\n continue\n }\n\n // unparseable colors crash RN\n // interpolation — apply them as a static style instead\n if (colorStyleKey[key] && !isAnimatableColor(val)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (key !== 'transform') {\n animateStyles.current[key] = update(key, animatedValueFor(key), val)\n seenAnimateKeys.add(key)\n continue\n }\n // key: 'transform'\n // for now just support one transform key\n if (!val) continue\n if (typeof val === 'string') {\n console.warn(`Warning: Tamagui can't animate string transforms yet!`)\n continue\n }\n\n sawTransform = true\n for (const transform of val) {\n if (!transform) continue\n const index = transformCount++\n // tkey: e.g: 'translateX'\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n animatedTranforms.current = [...animatedTranforms.current]\n }\n }\n\n // the derived leading: fontSize and the ratio each animate on their own\n // resolved entry, and the painted leading is their product, so\n // lineHeight is fontSize * ratio on every frame by construction rather\n // than by two solvers happening to agree. a key the transition does not\n // cover gets snapConfig here exactly as it would anywhere else, which\n // makes `transition=\"fontSize 300ms\"` move the leading with the size and\n // `transition=\"lineHeight 300ms\"` move the ratio over a size that has\n // already arrived.\n // the ratio is a factor exactly like the font size: a value of its own\n // while it moves, the number it already paints while it does not\n const ratioFactor = (): Animated.Value | number => {\n if (!movesLeading) {\n leadingRatioValue.current = null\n return paintedRatio!\n }\n leadingRatioValue.current = update(\n 'lineHeight',\n leadingRatioValueFrom(),\n paintedRatio!\n )\n return leadingRatioValue.current\n }\n let derivedLeading: ReturnType<typeof Animated.multiply> | undefined\n if (derivesLeading) {\n // a factor at rest multiplies as the plain number it already paints,\n // so the product does not carry a value nothing is moving\n let fontSizeFactor: Animated.Value | number = style.fontSize\n if (movesFontSize) {\n fontSizeFactor = update(\n 'fontSize',\n animatedValueFor('fontSize'),\n style.fontSize\n )\n animateStyles.current.fontSize = fontSizeFactor\n seenAnimateKeys.add('fontSize')\n }\n derivedLeading = Animated.multiply(fontSizeFactor, ratioFactor())\n } else if (inheritedFontSize) {\n derivedLeading = Animated.multiply(inheritedFontSize, ratioFactor())\n } else if (!isDisabled) {\n leadingRatioValue.current = null\n }\n paintsDerivedLeading.current = !!derivedLeading\n\n // what descendants inherit from this node: its own animated font size\n // when it has one, nothing when it has a font size of its own that is\n // not animating (react-native inherits that statically), and otherwise\n // whatever it inherited itself.\n const ownFontSizeNode = fontSizeNode.current\n const textChannel = ownFontSizeNode\n ? textChannelRef.current?.fontSize === ownFontSizeNode\n ? textChannelRef.current\n : { fontSize: ownFontSizeNode, driver: 'react-native' }\n : typeof style.fontSize === 'number'\n ? null\n : (inheritedText ?? null)\n textChannelRef.current = textChannel\n\n // drop stale Animated.Values whose keys left the incoming style, so the\n // key genuinely leaves the rendered style object (a released height goes\n // back to auto instead of staying pinned at its last pixel value). skip\n // while exiting (presence still animates the leaving keys) and while\n // disabled (the loop above intentionally skips every key). an active\n // pseudo owns the current emitted style until its matching release.\n if (!isExiting && !isDisabled && !pseudoActiveRef.current) {\n for (const k in animateStyles.current) {\n if (!seenAnimateKeys.has(k)) delete animateStyles.current[k]\n }\n if (!sawTransform) {\n if (animatedTranforms.current.length) animatedTranforms.current = []\n } else if (animatedTranforms.current.length > transformCount) {\n animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)\n }\n }\n\n const animatedTransformStyle =\n animatedTranforms.current.length > 0\n ? {\n transform: animatedTranforms.current.map((r) => {\n const key = Object.keys(r)[0]\n const val =\n animationsState.current!.get(r[key])?.interpolation || r[key]\n return { [key]: val }\n }),\n }\n : {}\n\n const animatedStyle = {\n ...Object.fromEntries(\n Object.entries(animateStyles.current).map(([k, v]) => [\n k,\n animationsState.current!.get(v)?.interpolation || v,\n ])\n ),\n ...(derivedLeading ? { lineHeight: derivedLeading } : null),\n ...animatedTransformStyle,\n }\n\n if (!isDisabled) {\n paintedTextRef.current = {\n fontSize: style.fontSize,\n lineHeight: style.lineHeight,\n ratio: paintedRatio,\n }\n }\n\n return {\n runners,\n completions,\n textChannel,\n style: [nonAnimatedStyle, animatedStyle],\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColorStyleKey = colorStyleKey[key]\n const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)\n let animateToValue = val\n const value = animated || new Animated.Value(val)\n const curInterpolation = animationsState.current.get(value)\n\n let interpolateArgs: any\n if (type) {\n interpolateArgs = getInterpolated(\n curInterpolation?.current ?? value['_value'],\n val,\n type\n )\n animationsState.current!.set(value, {\n interpolation: value.interpolate(interpolateArgs),\n current: val,\n })\n }\n\n if (isColorStyleKey) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n interpolateArgs = getColorInterpolated(\n curInterpolation?.current as string,\n // valIn is the next color\n valIn as string,\n animateToValue\n )\n animationsState.current!.set(value, {\n current: valIn,\n interpolation: value.interpolate(interpolateArgs),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n if (value) {\n const animationConfig = getAnimationConfig(\n key,\n animations,\n effectiveTransition,\n animationState\n )\n\n let resolve\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n completions.push(promise)\n\n runners.push(() => {\n value.stopAnimation()\n\n // `delay` drives the sequence below, so it must not also ride\n // along in the config or every delayed animation waits twice\n const { type, delay, ...config } = animationConfig\n const animation = Animated[type || 'spring'](value, {\n toValue: animateToValue,\n ...config,\n useNativeDriver: useNativeDriverForNode,\n })\n const animation2 = delay\n ? Animated.sequence([Animated.delay(delay), animation])\n : animation\n\n animation2.start(({ finished }) => {\n // always resolve during exit (element is leaving anyway)\n // for non-exit, only resolve on successful completion\n if (finished || isExiting) {\n resolve()\n }\n })\n })\n }\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n // prettier-ignore\n console.info(\n ' 💠 animate',\n key,\n `from (${value['_value']}) to`,\n valIn,\n `(${val})`,\n 'type',\n type,\n 'interpolate',\n interpolateArgs\n )\n }\n }\n return value\n }\n }, args)\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\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 useIsomorphicLayoutEffect(() => {\n res.runners.forEach((r) => r())\n\n // capture current cycle id\n const cycleId = exitCycleIdRef.current\n\n const cause: 'enter' | 'exit' | 'update' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'update'\n\n // interruptions: an enter or update still in flight when exit begins is\n // reported as finished:false (its own completion promise won't resolve\n // because the animation was stopped, not finished).\n if (cause === 'exit') {\n if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', false)\n }\n if (updateInFlightRef.current) {\n updateInFlightRef.current = false\n updateCycleIdRef.current++\n emit('end', 'update', false)\n }\n }\n\n // in-place update: a genuine style change while mounted (not entering or\n // exiting). guard on the style signature so lifecycle-only re-renders\n // don't register as updates.\n if (cause === 'update') {\n const sig = args[0] as string\n if (prevStyleSigRef.current === null || prevStyleSigRef.current === sig) {\n prevStyleSigRef.current = sig\n return\n }\n prevStyleSigRef.current = sig\n if (res.completions.length === 0) return\n if (updateInFlightRef.current) {\n // superseded before finishing\n emit('end', 'update', false)\n }\n updateInFlightRef.current = true\n const uid = ++updateCycleIdRef.current\n emit('start', 'update')\n Promise.all(res.completions).then(() => {\n if (uid !== updateCycleIdRef.current) return\n updateInFlightRef.current = false\n emit('end', 'update', true)\n })\n return\n }\n\n // keep the update signature current while entering/exiting\n prevStyleSigRef.current = args[0] as string\n\n // handle zero-completion case immediately (enter/exit report a pair)\n if (res.completions.length === 0) {\n emit('start', cause)\n emit('end', cause, true)\n if (isExiting && !exitCompletedRef.current) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n return\n }\n\n // enter/exit start (once per cycle; re-runs continue the same animation)\n if (cause === 'enter' && !enterStartedRef.current) {\n enterStartedRef.current = true\n emit('start', 'enter')\n }\n if (cause === 'exit' && !exitStartedRef.current) {\n exitStartedRef.current = true\n emit('start', 'exit')\n }\n\n Promise.all(res.completions).then(() => {\n // guard against stale cycle completion\n if (isExiting && cycleId !== exitCycleIdRef.current) return\n if (isExiting && exitCompletedRef.current) return\n\n if (isExiting) {\n if (exitStartedRef.current) {\n exitStartedRef.current = false\n // exit 'end' fires immediately before presence safeToRemove\n emit('end', 'exit', true)\n }\n exitCompletedRef.current = true\n sendExitComplete?.()\n } else if (enterStartedRef.current) {\n enterStartedRef.current = false\n emit('end', 'enter', true)\n }\n })\n }, args)\n\n // avoidReRenders: receive style changes imperatively from tamagui\n // and update Animated.Values directly without React re-renders\n // reuses the same update() + runner pattern as the useMemo path\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, any>,\n emittedTransition: TransitionProp | null | undefined,\n pseudoActive?: boolean,\n nextMetrics?: NativeTextMetrics\n ) => {\n pseudoActiveRef.current = pseudoActive === true\n const runners: Function[] = []\n const seenAnimateKeys = new Set<string>()\n let transformCount = 0\n let animatedShapeChanged = false\n // the emitter runs on a mounted node, so `default` is the state, but\n // the transition is the one it was handed\n const emittedResolved = forAnimationState(\n resolveTransition(emittedTransition ?? effectiveTransition, { animations }),\n 'default'\n )\n const emittedRatio = getLeadingRatio(nextMetrics)\n const emitterPaintsProduct = paintsLeadingProduct(nextStyle, emittedRatio)\n // the same rule as the render path: a text metric joins the graph only\n // while it is moving, so a pseudo that only changes an opacity leaves\n // the node's font size out of it and keeps the native driver\n const emitterMovesFontSize = movesTextValue('fontSize', nextStyle.fontSize)\n const emitterMovesLeading = emitterPaintsProduct\n ? movesLeadingRatio(emittedRatio)\n : movesTextValue('lineHeight', nextStyle.lineHeight)\n // nextStyle is the complete style for this node, so the emitter makes\n // the same single driver decision as the render path. include the\n // currently rendered graph because its stale keys are not removed until\n // the structural-change commit below.\n const useNativeDriverForNode =\n nativeDriver &&\n !hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&\n !emitterMovesFontSize &&\n !emitterMovesLeading &&\n !inheritedFontSizeNode(nextStyle, emittedRatio, inheritedText) &&\n !Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])\n\n const emitterDerivesLeading =\n emitterPaintsProduct &&\n (emitterMovesFontSize || emitterMovesLeading) &&\n (!!getTransitionForKey(emittedResolved, 'fontSize') ||\n !!getTransitionForKey(emittedResolved, 'lineHeight'))\n\n for (const key in nextStyle) {\n const rawVal = nextStyle[key]\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (emitterDerivesLeading && (key === 'fontSize' || key === 'lineHeight')) {\n continue\n }\n\n // a metric at rest is not the emitter's to animate. it either already\n // paints as a plain style or the commit below hands it to one.\n if (\n textMetricStyleKey[key] &&\n !(key === 'fontSize' ? emitterMovesFontSize : emitterMovesLeading)\n ) {\n continue\n }\n\n if (key === 'transform' && Array.isArray(val)) {\n for (const transform of val) {\n if (!transform) continue\n const index = transformCount++\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n if (!currentTransform) animatedShapeChanged = true\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n }\n } else if (\n animatedStyleKey[key] != null ||\n costlyToAnimateStyleKey[key] ||\n jsDriverStyleKey[key]\n ) {\n // layout keys only animate numbers ('auto'/percents are static);\n // unparseable themed colors can't be interpolated — skip both and\n // let the next render apply them statically\n if (jsDriverStyleKey[key] && typeof val !== 'number') continue\n if (colorStyleKey[key] && !isAnimatableColor(val)) continue\n if (!animateStyles.current[key]) animatedShapeChanged = true\n animateStyles.current[key] = update(key, animatedValueFor(key), val)\n seenAnimateKeys.add(key)\n }\n }\n\n // the emitter can only re-target Animated.Values the committed style\n // already paints. re-targeting the font size and the ratio keeps the\n // product moving without a commit; a pass that disagrees with the\n // committed style about whether the leading IS a product (a pseudo that\n // overrides it with an absolute length, or the first pseudo pass on a\n // node whose render never derived) needs the node swapped, which only a\n // commit can do.\n if (emitterDerivesLeading) {\n if (emitterMovesFontSize) {\n if (!animateStyles.current.fontSize) animatedShapeChanged = true\n animateStyles.current.fontSize = update(\n 'fontSize',\n animatedValueFor('fontSize'),\n nextStyle.fontSize as number\n )\n seenAnimateKeys.add('fontSize')\n }\n // a factor at rest is left out: it multiplies as the number the\n // painted product already carries. giving one a value the committed\n // style does not paint is the shape change that commits.\n if (emitterMovesLeading) {\n if (!leadingRatioValue.current) animatedShapeChanged = true\n leadingRatioValue.current = update(\n 'lineHeight',\n leadingRatioValueFrom(),\n emittedRatio!\n )\n }\n }\n if (emitterDerivesLeading !== paintsDerivedLeading.current) {\n animatedShapeChanged = true\n }\n\n // the emitter receives a complete style. keep the Animated style graph\n // equally complete, including a pseudo release that omits a pseudo-only\n // key. React Native needs a commit when that graph's shape changes.\n for (const key in animateStyles.current) {\n if (!seenAnimateKeys.has(key)) {\n delete animateStyles.current[key]\n animatedShapeChanged = true\n }\n }\n if (animatedTranforms.current.length > transformCount) {\n animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)\n animatedShapeChanged = true\n }\n\n paintedTextRef.current = {\n fontSize: nextStyle.fontSize,\n lineHeight: nextStyle.lineHeight,\n ratio: emittedRatio,\n }\n\n // run the queued animations immediately\n runners.forEach((r) => r())\n\n // pseudo state normally stays on the avoidReRenders path. adding or\n // removing a style key cannot be expressed by an existing Animated.Value,\n // so commit the pending state only for that structural change.\n if (animatedShapeChanged && stateRef.current.nextState) {\n stateRef.current.baseSetStateShallow?.(stateRef.current.nextState)\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColor = colorStyleKey[key]\n const [numVal, type] = isColor ? [0, undefined] : getValue(valIn)\n let animateToValue = numVal\n const value = animated || new Animated.Value(numVal)\n const curInterpolation = animationsState.current.get(value)\n\n if (type) {\n animationsState.current.set(value, {\n interpolation: value.interpolate(\n getInterpolated(\n curInterpolation?.current ?? value['_value'],\n numVal,\n type\n )\n ),\n current: numVal,\n })\n }\n\n if (isColor) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n animationsState.current.set(value, {\n current: valIn,\n interpolation: value.interpolate(\n getColorInterpolated(\n curInterpolation?.current as string,\n valIn as string,\n animateToValue\n )\n ),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n // the emitter runs for pseudo-state changes on a mounted node, so\n // `default` is the state, but the transition is the one it was handed\n const animationConfig = getAnimationConfig(\n key,\n animations,\n emittedTransition ?? effectiveTransition,\n 'default'\n )\n runners.push(() => {\n value.stopAnimation()\n const { type, delay, ...config } = animationConfig\n const anim = Animated[type || 'spring'](value, {\n toValue: animateToValue,\n ...config,\n useNativeDriver: useNativeDriverForNode,\n })\n ;(delay ? Animated.sequence([Animated.delay(delay), anim]) : anim).start()\n })\n\n return value\n }\n }\n useStyleEmitter?.(onEmittedStyle)\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n console.info(`Animated`, { response: res, inputStyle: style, isExiting })\n }\n }\n\n return res\n },\n }\n}\n\nfunction getColorInterpolated(\n currentColor: string | undefined,\n nextColor: string,\n animateToValue: number\n) {\n const inputRange = [0, 1]\n const outputRange = [currentColor ? currentColor : nextColor, nextColor]\n if (animateToValue === 0) {\n // because we are animating from value 1 to 0, we need to put target color at the beginning\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getInterpolated(current: number, next: number, postfix = 'deg') {\n if (next === current) {\n current = next - 0.000000001\n }\n const inputRange = [current, next]\n const outputRange = [`${current}${postfix}`, `${next}${postfix}`]\n if (next < current) {\n inputRange.reverse()\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\n/**\n * one resolved entry as a react-native Animated config.\n *\n * springs go in as stiffness/damping/mass, which is the parameterization RN\n * actually integrates. `bounciness`/`speed` and `tension`/`friction` are older\n * spellings of the same two numbers, so nothing is lost by not using them.\n */\nfunction entryToRN(entry: ResolvedEntry): AnimationConfig {\n const extra = entry.timing.kind === 'spring' ? entry.timing.extra : undefined\n\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 ...(typeof extra?.velocity === 'number' ? { velocity: extra.velocity } : null),\n ...(typeof extra?.overshootClamping === 'boolean'\n ? { overshootClamping: extra.overshootClamping }\n : null),\n ...(entry.delayMs ? { delay: entry.delayMs } : null),\n }\n }\n\n const bezier = easingToBezier(entry.timing.easing)\n return {\n type: 'timing',\n duration: entry.timing.durationMs,\n // `linear()` and `steps()` have no bezier equivalent; RN's default easing\n // is the honest answer rather than a curve we made up\n ...(bezier\n ? { easing: Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) }\n : null),\n ...(entry.delayMs ? { delay: entry.delayMs } : null),\n }\n}\n\n// a key the transition does not cover does not animate. snapping is what css\n// does for an unlisted property, so the drivers have to agree on it too.\nconst snapConfig: AnimationConfig = { type: 'timing', duration: 0 }\n\nfunction getAnimationConfig(\n key: string,\n animations: AnimationsConfig,\n transition?: TransitionProp | null,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n): AnimationConfig {\n const resolved = forAnimationState(\n resolveTransition(transition, { animations }),\n animationState\n )\n const entry = getTransitionForKey(resolved, key)\n return entry ? entryToRN(entry) : snapConfig\n}\n\nfunction getValue(input: number | string, isColor = false) {\n if (typeof input !== 'string') {\n return [input] as const\n }\n // the unit is optional: unitless numbers reach here as strings (scale, and\n // any bare token value), and the number may be fractional. matching only\n // `[-0-9]+` followed by a required unit read \"1.5deg\" as 5 and gave NaN for\n // \"0.95\", and an Animated animation toward NaN never calls its completion\n // callback, which strands whatever waits on it.\n const [_, number, after] = input.match(/(-?(?:\\d+\\.?\\d*|\\.\\d+))(deg|%|px)?/) ?? []\n return [+number, after] as const\n}\n"
10
10
  ]
11
11
  }