@tamagui/animations-react-native 2.7.7 → 3.0.0-beta.1097.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.
Files changed (46) hide show
  1. package/dist/cjs/createAnimations.native.cjs +692 -0
  2. package/dist/cjs/createAnimations.native.js +643 -597
  3. package/dist/cjs/createAnimations.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +19 -13
  5. package/dist/cjs/index.native.cjs +21 -0
  6. package/dist/cjs/index.native.js +12 -13
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/polyfill.cjs +2 -2
  9. package/dist/cjs/polyfill.native.cjs +5 -0
  10. package/dist/cjs/polyfill.native.js +2 -1
  11. package/dist/cjs/polyfill.native.js.map +1 -1
  12. package/dist/cjs/types.cjs +16 -0
  13. package/dist/cjs/types.native.cjs +18 -0
  14. package/dist/cjs/types.native.js +20 -0
  15. package/dist/cjs/types.native.js.map +1 -0
  16. package/dist/esm/createAnimations.native.js +621 -567
  17. package/dist/esm/createAnimations.native.js.map +1 -1
  18. package/dist/esm/index.js +6 -2
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/index.mjs +6 -2
  21. package/dist/esm/index.mjs.map +1 -1
  22. package/dist/esm/index.native.js +2 -2
  23. package/dist/esm/polyfill.mjs +2 -1
  24. package/dist/esm/polyfill.mjs.map +1 -1
  25. package/dist/esm/polyfill.native.js +2 -1
  26. package/dist/esm/polyfill.native.js.map +1 -1
  27. package/dist/esm/types.mjs +0 -0
  28. package/dist/esm/types.native.js +0 -0
  29. package/package.json +15 -10
  30. package/src/{createAnimations.tsx → createAnimations.native.tsx} +277 -137
  31. package/src/index.native.ts +3 -0
  32. package/src/index.ts +20 -2
  33. package/src/types.ts +4 -0
  34. package/types/{createAnimations.d.ts → createAnimations.native.d.ts} +5 -14
  35. package/types/createAnimations.native.d.ts.map +11 -0
  36. package/types/index.d.ts +10 -2
  37. package/types/index.d.ts.map +2 -2
  38. package/types/index.native.d.ts +4 -0
  39. package/types/index.native.d.ts.map +11 -0
  40. package/types/types.d.ts +5 -0
  41. package/types/types.d.ts.map +11 -0
  42. package/dist/cjs/createAnimations.cjs +0 -566
  43. package/dist/esm/createAnimations.mjs +0 -524
  44. package/dist/esm/createAnimations.mjs.map +0 -1
  45. package/dist/esm/index.native.js.map +0 -1
  46. package/types/createAnimations.d.ts.map +0 -11
@@ -1,17 +1,35 @@
1
- import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'
1
+ import {
2
+ easingToBezier,
3
+ forAnimationState,
4
+ getTransitionForKey,
5
+ resolveTransition,
6
+ type AnimationsConfig,
7
+ type ResolvedEntry,
8
+ type ResolvedTransition,
9
+ } from '@tamagui/animation-helpers'
2
10
  import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
3
11
  import { ResetPresence, usePresence } from '@tamagui/use-presence'
4
12
  import type {
5
13
  AnimatedNumberStrategy,
6
- AnimationDriver,
14
+ AnimationDriverWithAnimatedNumbers,
7
15
  TransitionProp,
8
16
  UniversalAnimatedNumber,
9
17
  UseAnimatedNumberReaction,
10
18
  UseAnimatedNumberStyle,
11
19
  } from '@tamagui/web'
12
- import { useEvent, useThemeWithState } from '@tamagui/web'
20
+ import { useEvent } from '@tamagui/web'
21
+ import { useThemeWithState } from '@tamagui/web/internal-runtime'
13
22
  import React from 'react'
14
- import { Animated, type Text, type View } from 'react-native'
23
+ import {
24
+ Animated,
25
+ Easing,
26
+ processColor,
27
+ type ColorValue,
28
+ type Text,
29
+ type View,
30
+ } from 'react-native'
31
+
32
+ import type { CreateAnimationsOptions } from './types'
15
33
 
16
34
  // detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys
17
35
  const isFabric =
@@ -26,27 +44,17 @@ const resolveDynamicValue = (value: any, isDark: boolean): any => {
26
44
  return value
27
45
  }
28
46
 
29
- type AnimationsConfig<A extends object = any> = { [Key in keyof A]: AnimationConfig }
30
-
31
- type SpringConfig = { type?: 'spring' } & Partial<
32
- Pick<
33
- Animated.SpringAnimationConfig,
34
- | 'delay'
35
- | 'bounciness'
36
- | 'damping'
37
- | 'friction'
38
- | 'mass'
39
- | 'overshootClamping'
40
- | 'speed'
41
- | 'stiffness'
42
- | 'tension'
43
- | 'velocity'
44
- >
45
- >
46
-
47
- type TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>
48
-
49
- type AnimationConfig = SpringConfig | TimingConfig
47
+ /** what `getAnimationConfig` hands to `Animated.spring` / `Animated.timing` */
48
+ type AnimationConfig =
49
+ | ({ type: 'spring'; delay?: number } & Partial<
50
+ Pick<
51
+ Animated.SpringAnimationConfig,
52
+ 'damping' | 'mass' | 'overshootClamping' | 'stiffness' | 'velocity'
53
+ >
54
+ >)
55
+ | ({ type: 'timing'; delay?: number } & Partial<
56
+ Pick<Animated.TimingAnimationConfig, 'duration' | 'easing'>
57
+ >)
50
58
 
51
59
  const animatedStyleKey = {
52
60
  transform: true,
@@ -77,25 +85,21 @@ const layoutStyleKey = {
77
85
  function hasAnimatedLayoutKey(
78
86
  style: Record<string, any>,
79
87
  isDark: boolean,
80
- animateOnly?: string[]
88
+ resolved: ResolvedTransition
81
89
  ) {
82
90
  for (const key in layoutStyleKey) {
83
- if (animateOnly && !animateOnly.includes(key)) continue
91
+ if (!getTransitionForKey(resolved, key)) continue
84
92
  if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true
85
93
  }
86
94
  return false
87
95
  }
88
96
 
89
- // a color string that RN's color interpolation can actually parse. var(...),
90
- // calc(...) and empty strings reach createInterpolationFromStringOutputRange /
91
- // mapStringToNumericComponents and throw ('.map of null' / 'outputRange must
92
- // contain color or value with numeric component'). those must be applied as a
93
- // static style instead of entering interpolation.
97
+ // Only colors accepted by RN's own parser can enter interpolation. CSS-wide
98
+ // keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach
99
+ // createInterpolationFromStringOutputRange / mapStringToNumericComponents and
100
+ // throw. Those values must be applied as static styles.
94
101
  function isAnimatableColor(value: unknown): value is string {
95
- if (typeof value !== 'string') return false
96
- if (value === '') return false
97
- if (value.includes('var(') || value.includes('calc(')) return false
98
- return true
102
+ return typeof value === 'string' && processColor(value as ColorValue) != null
99
103
  }
100
104
 
101
105
  // these style keys are costly to animate and only work with native driver on Fabric
@@ -113,11 +117,6 @@ const costlyToAnimateStyleKey = {
113
117
  ...colorStyleKey,
114
118
  }
115
119
 
116
- type CreateAnimationsOptions = {
117
- // override native driver detection (default: auto-detect Fabric)
118
- useNativeDriver?: boolean
119
- }
120
-
121
120
  export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
122
121
  export const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text
123
122
 
@@ -158,7 +157,12 @@ export function useAnimatedNumber(
158
157
  : undefined
159
158
 
160
159
  if (type === 'direct') {
160
+ state.current.composite?.stop()
161
+ state.current.composite = null
161
162
  val.setValue(next)
163
+ // a direct set finishes the moment it lands. not calling back stranded
164
+ // everything waiting on it (sheet snap, presence completion).
165
+ onFinish?.()
162
166
  } else if (type === 'spring') {
163
167
  state.current.composite?.stop()
164
168
  const composite = Animated.spring(val, {
@@ -204,7 +208,33 @@ export const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (
204
208
  value,
205
209
  getStyle
206
210
  ) => {
207
- return getStyle(value.getInstance())
211
+ const instance = value.getInstance()
212
+ const animatedStyle = getStyle(instance)
213
+ const usesAnimatedNode = hasAnimatedNode(animatedStyle)
214
+ const [current, setCurrent] = React.useState(value.getValue())
215
+
216
+ // preserve the native animated-node path for direct mappings. callbacks
217
+ // that do arithmetic require numeric values, so drive those through the
218
+ // value listener and render the computed style.
219
+ React.useEffect(() => {
220
+ if (usesAnimatedNode) return
221
+
222
+ const id = instance.addListener(({ value: next }) => {
223
+ setCurrent(next)
224
+ })
225
+ return () => {
226
+ instance.removeListener(id)
227
+ }
228
+ }, [instance, usesAnimatedNode])
229
+
230
+ return usesAnimatedNode ? animatedStyle : getStyle(current)
231
+ }
232
+
233
+ function hasAnimatedNode(value: unknown): boolean {
234
+ if (!value || typeof value !== 'object') return false
235
+ if (typeof (value as any).__getValue === 'function') return true
236
+ if (Array.isArray(value)) return value.some(hasAnimatedNode)
237
+ return Object.values(value).some(hasAnimatedNode)
208
238
  }
209
239
 
210
240
  export const useAnimatedNumbersStyle = (
@@ -217,11 +247,10 @@ export const useAnimatedNumbersStyle = (
217
247
  export function createAnimations<A extends AnimationsConfig>(
218
248
  animations: A,
219
249
  options?: CreateAnimationsOptions
220
- ): AnimationDriver<A> {
250
+ ): AnimationDriverWithAnimatedNumbers<A> {
221
251
  const nativeDriver = options?.useNativeDriver ?? isFabric
222
252
 
223
253
  return {
224
- isReactNative: true,
225
254
  inputStyle: 'value',
226
255
  outputStyle: 'inline',
227
256
  avoidReRenders: true,
@@ -237,16 +266,36 @@ export function createAnimations<A extends AnimationsConfig>(
237
266
  ResetPresence,
238
267
  useAnimations: ({
239
268
  props,
240
- onDidAnimate,
269
+ onTransition,
241
270
  style,
242
271
  componentState,
243
272
  presence,
244
273
  stateRef,
274
+ styleState,
245
275
  useStyleEmitter,
246
276
  }) => {
247
277
  const isDisabled = isWeb && componentState.unmounted === true
248
278
  const isExiting = presence?.[0] === false
249
279
  const sendExitComplete = presence?.[1]
280
+ const onTransitionRef = React.useRef(onTransition)
281
+ onTransitionRef.current = onTransition
282
+ const emit = (
283
+ phase: 'start' | 'end',
284
+ cause: 'enter' | 'exit' | 'update',
285
+ finished?: boolean
286
+ ) => {
287
+ onTransitionRef.current?.(
288
+ phase === 'end' ? { phase, cause, finished } : { phase, cause }
289
+ )
290
+ }
291
+ // createComponent merges a colocated `transition` out of the active
292
+ // pseudo style (`enterStyle={{ opacity: 0, transition: '200ms' }}`), so
293
+ // this is the one that applies right now, not the base prop.
294
+ const effectiveTransition = (styleState?.effectiveTransition ?? props.transition) as
295
+ | TransitionProp
296
+ | null
297
+ | undefined
298
+
250
299
  const [, themeState] = useThemeWithState({})
251
300
  // Check scheme first, then fall back to checking theme name for 'dark'
252
301
  const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
@@ -272,6 +321,13 @@ export function createAnimations<A extends AnimationsConfig>(
272
321
  const exitCompletedRef = React.useRef(false)
273
322
  const wasExitingRef = React.useRef(false)
274
323
 
324
+ // onTransition lifecycle bookkeeping
325
+ const enterStartedRef = React.useRef(false)
326
+ const exitStartedRef = React.useRef(false)
327
+ const updateInFlightRef = React.useRef(false)
328
+ const updateCycleIdRef = React.useRef(0)
329
+ const prevStyleSigRef = React.useRef<string | null>(null)
330
+
275
331
  // detect transition into/out of exiting state
276
332
  const justStartedExiting = isExiting && !wasExitingRef.current
277
333
  const justStoppedExiting = !isExiting && wasExitingRef.current
@@ -286,9 +342,6 @@ export function createAnimations<A extends AnimationsConfig>(
286
342
  exitCycleIdRef.current++
287
343
  }
288
344
 
289
- const animateOnly = (props.animateOnly as string[]) || []
290
- const hasTransitionOnly = !!props.animateOnly
291
-
292
345
  // Track if we just finished entering (transition from entering to not entering)
293
346
  // must be declared before args array that uses justFinishedEntering
294
347
  const isEntering = !!componentState.unmounted
@@ -300,12 +353,12 @@ export function createAnimations<A extends AnimationsConfig>(
300
353
 
301
354
  const args = [
302
355
  JSON.stringify(style),
356
+ JSON.stringify(effectiveTransition),
303
357
  componentState,
304
358
  isExiting,
305
- !!onDidAnimate,
359
+ !!onTransition,
306
360
  isDark,
307
361
  justFinishedEntering,
308
- hasTransitionOnly,
309
362
  ]
310
363
 
311
364
  const res = React.useMemo(() => {
@@ -320,17 +373,19 @@ export function createAnimations<A extends AnimationsConfig>(
320
373
  ? 'enter'
321
374
  : 'default'
322
375
 
376
+ // which style keys animate at all is the transition's own decision, so
377
+ // a property list narrows this the same way it narrows css
378
+ const resolved = forAnimationState(
379
+ resolveTransition(effectiveTransition, { animations }),
380
+ animationState
381
+ )
382
+
323
383
  const nonAnimatedStyle = {}
324
384
  // animatedStyle owns every Animated.Value on the node. Fabric cannot mix
325
385
  // native- and JS-driven values inside that shared graph, so one layout
326
386
  // animation makes the whole node use the JS driver.
327
387
  const useNativeDriverForNode =
328
- nativeDriver &&
329
- !hasAnimatedLayoutKey(
330
- style,
331
- isDark,
332
- hasTransitionOnly ? animateOnly : undefined
333
- )
388
+ nativeDriver && !hasAnimatedLayoutKey(style, isDark, resolved)
334
389
 
335
390
  // track which animated keys/transforms the incoming style actually
336
391
  // carries this pass, so entries that left the style can be dropped
@@ -342,7 +397,7 @@ export function createAnimations<A extends AnimationsConfig>(
342
397
 
343
398
  for (const key in style) {
344
399
  const rawVal = style[key]
345
- // Resolve dynamic theme values (like $theme-dark)
400
+ // Resolve dynamic theme values from flat theme clauses.
346
401
  const val = resolveDynamicValue(rawVal, isDark)
347
402
  if (val === undefined) continue
348
403
 
@@ -359,7 +414,10 @@ export function createAnimations<A extends AnimationsConfig>(
359
414
  continue
360
415
  }
361
416
 
362
- if (hasTransitionOnly && !animateOnly.includes(key)) {
417
+ // `transform` is a container, not a property: its parts each resolve
418
+ // on their own below, and a part no entry covers gets `snapConfig`.
419
+ // the array cannot be split into animated and static halves here.
420
+ if (key !== 'transform' && !getTransitionForKey(resolved, key)) {
363
421
  nonAnimatedStyle[key] = val
364
422
  continue
365
423
  }
@@ -371,7 +429,7 @@ export function createAnimations<A extends AnimationsConfig>(
371
429
  continue
372
430
  }
373
431
 
374
- // unparseable themed colors (var(), calc(), empty) crash RN
432
+ // unparseable colors crash RN
375
433
  // interpolation — apply them as a static style instead
376
434
  if (colorStyleKey[key] && !isAnimatableColor(val)) {
377
435
  nonAnimatedStyle[key] = val
@@ -493,7 +551,7 @@ export function createAnimations<A extends AnimationsConfig>(
493
551
  const animationConfig = getAnimationConfig(
494
552
  key,
495
553
  animations,
496
- props.transition,
554
+ effectiveTransition,
497
555
  animationState
498
556
  )
499
557
 
@@ -506,22 +564,19 @@ export function createAnimations<A extends AnimationsConfig>(
506
564
  runners.push(() => {
507
565
  value.stopAnimation()
508
566
 
509
- function getAnimation() {
510
- return Animated[animationConfig.type || 'spring'](value, {
511
- toValue: animateToValue,
512
- ...animationConfig,
513
- useNativeDriver: useNativeDriverForNode,
514
- })
515
- }
516
-
517
- const animation = animationConfig.delay
518
- ? Animated.sequence([
519
- Animated.delay(animationConfig.delay),
520
- getAnimation(),
521
- ])
522
- : getAnimation()
567
+ // `delay` drives the sequence below, so it must not also ride
568
+ // along in the config or every delayed animation waits twice
569
+ const { type, delay, ...config } = animationConfig
570
+ const animation = Animated[type || 'spring'](value, {
571
+ toValue: animateToValue,
572
+ ...config,
573
+ useNativeDriver: useNativeDriverForNode,
574
+ })
575
+ const animation2 = delay
576
+ ? Animated.sequence([Animated.delay(delay), animation])
577
+ : animation
523
578
 
524
- animation.start(({ finished }) => {
579
+ animation2.start(({ finished }) => {
525
580
  // always resolve during exit (element is leaving anyway)
526
581
  // for non-exit, only resolve on successful completion
527
582
  if (finished || isExiting) {
@@ -556,15 +611,74 @@ export function createAnimations<A extends AnimationsConfig>(
556
611
  wasExitingRef.current = isExiting
557
612
  })
558
613
 
614
+ // exit interrupted by a re-enter: report the exit as finished:false
615
+ useIsomorphicLayoutEffect(() => {
616
+ if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {
617
+ exitStartedRef.current = false
618
+ emit('end', 'exit', false)
619
+ }
620
+ }, [justStoppedExiting])
621
+
559
622
  useIsomorphicLayoutEffect(() => {
560
623
  res.runners.forEach((r) => r())
561
624
 
562
625
  // capture current cycle id
563
626
  const cycleId = exitCycleIdRef.current
564
627
 
565
- // handle zero-completion case immediately
628
+ const cause: 'enter' | 'exit' | 'update' = isExiting
629
+ ? 'exit'
630
+ : isEntering || justFinishedEntering
631
+ ? 'enter'
632
+ : 'update'
633
+
634
+ // interruptions: an enter or update still in flight when exit begins is
635
+ // reported as finished:false (its own completion promise won't resolve
636
+ // because the animation was stopped, not finished).
637
+ if (cause === 'exit') {
638
+ if (enterStartedRef.current) {
639
+ enterStartedRef.current = false
640
+ emit('end', 'enter', false)
641
+ }
642
+ if (updateInFlightRef.current) {
643
+ updateInFlightRef.current = false
644
+ updateCycleIdRef.current++
645
+ emit('end', 'update', false)
646
+ }
647
+ }
648
+
649
+ // in-place update: a genuine style change while mounted (not entering or
650
+ // exiting). guard on the style signature so lifecycle-only re-renders
651
+ // don't register as updates.
652
+ if (cause === 'update') {
653
+ const sig = args[0] as string
654
+ if (prevStyleSigRef.current === null || prevStyleSigRef.current === sig) {
655
+ prevStyleSigRef.current = sig
656
+ return
657
+ }
658
+ prevStyleSigRef.current = sig
659
+ if (res.completions.length === 0) return
660
+ if (updateInFlightRef.current) {
661
+ // superseded before finishing
662
+ emit('end', 'update', false)
663
+ }
664
+ updateInFlightRef.current = true
665
+ const uid = ++updateCycleIdRef.current
666
+ emit('start', 'update')
667
+ Promise.all(res.completions).then(() => {
668
+ if (uid !== updateCycleIdRef.current) return
669
+ updateInFlightRef.current = false
670
+ emit('end', 'update', true)
671
+ })
672
+ return
673
+ }
674
+
675
+ // keep the update signature current while entering/exiting
676
+ prevStyleSigRef.current = args[0] as string
677
+
678
+ // handle zero-completion case immediately (enter/exit report a pair)
566
679
  if (res.completions.length === 0) {
567
- onDidAnimate?.()
680
+ emit('start', cause)
681
+ emit('end', cause, true)
568
682
  if (isExiting && !exitCompletedRef.current) {
569
683
  exitCompletedRef.current = true
570
684
  sendExitComplete?.()
@@ -572,40 +686,58 @@ export function createAnimations<A extends AnimationsConfig>(
572
686
  return
573
687
  }
574
688
 
575
- let cancel = false
689
+ // enter/exit start (once per cycle; re-runs continue the same animation)
690
+ if (cause === 'enter' && !enterStartedRef.current) {
691
+ enterStartedRef.current = true
692
+ emit('start', 'enter')
693
+ }
694
+ if (cause === 'exit' && !exitStartedRef.current) {
695
+ exitStartedRef.current = true
696
+ emit('start', 'exit')
697
+ }
698
+
576
699
  Promise.all(res.completions).then(() => {
577
- if (cancel) return
578
700
  // guard against stale cycle completion
579
701
  if (isExiting && cycleId !== exitCycleIdRef.current) return
580
702
  if (isExiting && exitCompletedRef.current) return
581
703
 
582
- onDidAnimate?.()
583
704
  if (isExiting) {
705
+ if (exitStartedRef.current) {
706
+ exitStartedRef.current = false
707
+ // exit 'end' fires immediately before presence safeToRemove
708
+ emit('end', 'exit', true)
709
+ }
584
710
  exitCompletedRef.current = true
585
711
  sendExitComplete?.()
712
+ } else if (enterStartedRef.current) {
713
+ enterStartedRef.current = false
714
+ emit('end', 'enter', true)
586
715
  }
587
716
  })
588
- return () => {
589
- cancel = true
590
- }
591
717
  }, args)
592
718
 
593
719
  // avoidReRenders: receive style changes imperatively from tamagui
594
720
  // and update Animated.Values directly without React re-renders
595
721
  // reuses the same update() + runner pattern as the useMemo path
596
- useStyleEmitter?.((nextStyle, _effectiveTransition, pseudoActive) => {
722
+ useStyleEmitter?.((nextStyle, emittedTransition, pseudoActive) => {
597
723
  pseudoActiveRef.current = pseudoActive === true
598
724
  const runners: Function[] = []
599
725
  const seenAnimateKeys = new Set<string>()
600
726
  let transformCount = 0
601
727
  let animatedShapeChanged = false
728
+ // the emitter runs on a mounted node, so `default` is the state, but
729
+ // the transition is the one it was handed
730
+ const emittedResolved = forAnimationState(
731
+ resolveTransition(emittedTransition ?? effectiveTransition, { animations }),
732
+ 'default'
733
+ )
602
734
  // nextStyle is the complete style for this node, so the emitter makes
603
735
  // the same single driver decision as the render path. include the
604
736
  // currently rendered graph because its stale keys are not removed until
605
737
  // the structural-change commit below.
606
738
  const useNativeDriverForNode =
607
739
  nativeDriver &&
608
- !hasAnimatedLayoutKey(nextStyle, isDark) &&
740
+ !hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&
609
741
  !Object.keys(animateStyles.current).some((key) => layoutStyleKey[key])
610
742
 
611
743
  for (const key in nextStyle) {
@@ -703,23 +835,23 @@ export function createAnimations<A extends AnimationsConfig>(
703
835
  })
704
836
  }
705
837
 
838
+ // the emitter runs for pseudo-state changes on a mounted node, so
839
+ // `default` is the state, but the transition is the one it was handed
706
840
  const animationConfig = getAnimationConfig(
707
841
  key,
708
842
  animations,
709
- props.transition,
843
+ emittedTransition ?? effectiveTransition,
710
844
  'default'
711
845
  )
712
846
  runners.push(() => {
713
847
  value.stopAnimation()
714
- const anim = Animated[animationConfig.type || 'spring'](value, {
848
+ const { type, delay, ...config } = animationConfig
849
+ const anim = Animated[type || 'spring'](value, {
715
850
  toValue: animateToValue,
716
- ...animationConfig,
851
+ ...config,
717
852
  useNativeDriver: useNativeDriverForNode,
718
853
  })
719
- ;(animationConfig.delay
720
- ? Animated.sequence([Animated.delay(animationConfig.delay), anim])
721
- : anim
722
- ).start()
854
+ ;(delay ? Animated.sequence([Animated.delay(delay), anim]) : anim).start()
723
855
  })
724
856
 
725
857
  return value
@@ -770,62 +902,70 @@ function getInterpolated(current: number, next: number, postfix = 'deg') {
770
902
  }
771
903
  }
772
904
 
773
- function getAnimationConfig(
774
- key: string,
775
- animations: AnimationsConfig,
776
- transition?: TransitionProp,
777
- animationState: 'enter' | 'exit' | 'default' = 'default'
778
- ): AnimationConfig {
779
- const normalized = normalizeTransition(transition)
780
- const shortKey = transformShorthands[key]
781
-
782
- // Check for property-specific animation
783
- const propAnimation = normalized.properties[key] ?? normalized.properties[shortKey]
784
-
785
- let animationType: string | null = null
786
- let extraConf: any = {}
787
-
788
- if (typeof propAnimation === 'string') {
789
- // Direct animation name: { x: 'quick' }
790
- animationType = propAnimation
791
- } else if (propAnimation && typeof propAnimation === 'object') {
792
- // Config object: { x: { type: 'quick', delay: 100 } }
793
- // Use effective animation based on state if no explicit type in config
794
- animationType =
795
- propAnimation.type || getEffectiveAnimation(normalized, animationState)
796
- extraConf = propAnimation
797
- } else {
798
- // Fall back to effective animation based on state (enter/exit/default)
799
- animationType = getEffectiveAnimation(normalized, animationState)
800
- }
801
-
802
- // Apply global delay if no property-specific delay
803
- if (normalized.delay && !extraConf.delay) {
804
- extraConf = { ...extraConf, delay: normalized.delay }
905
+ /**
906
+ * one resolved entry as a react-native Animated config.
907
+ *
908
+ * springs go in as stiffness/damping/mass, which is the parameterization RN
909
+ * actually integrates. `bounciness`/`speed` and `tension`/`friction` are older
910
+ * spellings of the same two numbers, so nothing is lost by not using them.
911
+ */
912
+ function entryToRN(entry: ResolvedEntry): AnimationConfig {
913
+ const extra = entry.timing.kind === 'spring' ? entry.timing.extra : undefined
914
+
915
+ if (entry.timing.kind === 'spring') {
916
+ return {
917
+ type: 'spring',
918
+ stiffness: entry.timing.stiffness,
919
+ damping: entry.timing.damping,
920
+ mass: entry.timing.mass,
921
+ ...(typeof extra?.velocity === 'number' ? { velocity: extra.velocity } : null),
922
+ ...(typeof extra?.overshootClamping === 'boolean'
923
+ ? { overshootClamping: extra.overshootClamping }
924
+ : null),
925
+ ...(entry.delayMs ? { delay: entry.delayMs } : null),
926
+ }
805
927
  }
806
928
 
807
- const found = animationType ? animations[animationType] : {}
929
+ const bezier = easingToBezier(entry.timing.easing)
808
930
  return {
809
- ...found,
810
- // Apply global spring config overrides (from transition={['bouncy', { stiffness: 1000 }]})
811
- ...normalized.config,
812
- // Property-specific config takes highest precedence
813
- ...extraConf,
931
+ type: 'timing',
932
+ duration: entry.timing.durationMs,
933
+ // `linear()` and `steps()` have no bezier equivalent; RN's default easing
934
+ // is the honest answer rather than a curve we made up
935
+ ...(bezier
936
+ ? { easing: Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) }
937
+ : null),
938
+ ...(entry.delayMs ? { delay: entry.delayMs } : null),
814
939
  }
815
940
  }
816
941
 
817
- // try both combos
818
- const transformShorthands = {
819
- x: 'translateX',
820
- y: 'translateY',
821
- translateX: 'x',
822
- translateY: 'y',
942
+ // a key the transition does not cover does not animate. snapping is what css
943
+ // does for an unlisted property, so the drivers have to agree on it too.
944
+ const snapConfig: AnimationConfig = { type: 'timing', duration: 0 }
945
+
946
+ function getAnimationConfig(
947
+ key: string,
948
+ animations: AnimationsConfig,
949
+ transition?: TransitionProp | null,
950
+ animationState: 'enter' | 'exit' | 'default' = 'default'
951
+ ): AnimationConfig {
952
+ const resolved = forAnimationState(
953
+ resolveTransition(transition, { animations }),
954
+ animationState
955
+ )
956
+ const entry = getTransitionForKey(resolved, key)
957
+ return entry ? entryToRN(entry) : snapConfig
823
958
  }
824
959
 
825
960
  function getValue(input: number | string, isColor = false) {
826
961
  if (typeof input !== 'string') {
827
962
  return [input] as const
828
963
  }
829
- const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []
964
+ // the unit is optional: unitless numbers reach here as strings (scale, and
965
+ // any bare token value), and the number may be fractional. matching only
966
+ // `[-0-9]+` followed by a required unit read "1.5deg" as 5 and gave NaN for
967
+ // "0.95", and an Animated animation toward NaN never calls its completion
968
+ // callback, which strands whatever waits on it.
969
+ const [_, number, after] = input.match(/(-?(?:\d+\.?\d*|\.\d+))(deg|%|px)?/) ?? []
830
970
  return [+number, after] as const
831
971
  }
@@ -0,0 +1,3 @@
1
+ import './polyfill'
2
+
3
+ export * from './createAnimations.native'
package/src/index.ts CHANGED
@@ -1,3 +1,21 @@
1
- import './polyfill'
1
+ import type { AnimationsConfig } from '@tamagui/animation-helpers'
2
+ import type { AnimationDriverWithAnimatedNumbers } from '@tamagui/web'
2
3
 
3
- export * from './createAnimations'
4
+ import type { CreateAnimationsOptions } from './types'
5
+
6
+ export type { CreateAnimationsOptions }
7
+
8
+ const unsupportedWebDriverMessage =
9
+ '[@tamagui/animations-react-native] This animation driver only works on native. On web, use @tamagui/animations-css, @tamagui/animations-motion, or @tamagui/animations-reanimated.'
10
+
11
+ /**
12
+ * The web half of the native driver. It keeps the same signature so a shared
13
+ * config typechecks on both platforms, and throws if anything actually reaches
14
+ * for it: `Animated` lives in react-native, which web is not tied to.
15
+ */
16
+ export const createAnimations = <A extends AnimationsConfig>(
17
+ _animations: A,
18
+ _options?: CreateAnimationsOptions
19
+ ): AnimationDriverWithAnimatedNumbers<A> => {
20
+ throw new Error(unsupportedWebDriverMessage)
21
+ }