@onlynative/inertia 0.0.1-alpha.4 → 0.0.1-alpha.6

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 (48) hide show
  1. package/dist/index.d.mts +30 -4
  2. package/dist/index.d.ts +30 -4
  3. package/dist/index.js +123 -24
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +123 -25
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/motion/Image.d.mts +1 -1
  8. package/dist/motion/Image.d.ts +1 -1
  9. package/dist/motion/Image.js +100 -24
  10. package/dist/motion/Image.js.map +1 -1
  11. package/dist/motion/Image.mjs +100 -24
  12. package/dist/motion/Image.mjs.map +1 -1
  13. package/dist/motion/Pressable.d.mts +1 -1
  14. package/dist/motion/Pressable.d.ts +1 -1
  15. package/dist/motion/Pressable.js +100 -24
  16. package/dist/motion/Pressable.js.map +1 -1
  17. package/dist/motion/Pressable.mjs +100 -24
  18. package/dist/motion/Pressable.mjs.map +1 -1
  19. package/dist/motion/ScrollView.d.mts +1 -1
  20. package/dist/motion/ScrollView.d.ts +1 -1
  21. package/dist/motion/ScrollView.js +100 -24
  22. package/dist/motion/ScrollView.js.map +1 -1
  23. package/dist/motion/ScrollView.mjs +100 -24
  24. package/dist/motion/ScrollView.mjs.map +1 -1
  25. package/dist/motion/Text.d.mts +1 -1
  26. package/dist/motion/Text.d.ts +1 -1
  27. package/dist/motion/Text.js +100 -24
  28. package/dist/motion/Text.js.map +1 -1
  29. package/dist/motion/Text.mjs +100 -24
  30. package/dist/motion/Text.mjs.map +1 -1
  31. package/dist/motion/View.d.mts +1 -1
  32. package/dist/motion/View.d.ts +1 -1
  33. package/dist/motion/View.js +100 -24
  34. package/dist/motion/View.js.map +1 -1
  35. package/dist/motion/View.mjs +100 -24
  36. package/dist/motion/View.mjs.map +1 -1
  37. package/dist/{types-CjztO3RW.d.mts → types-NmNeJjo1.d.mts} +23 -2
  38. package/dist/{types-CjztO3RW.d.ts → types-NmNeJjo1.d.ts} +23 -2
  39. package/jest-preset.js +33 -0
  40. package/jest-setup.js +209 -0
  41. package/package.json +5 -1
  42. package/src/index.ts +1 -0
  43. package/src/motion/createMotionComponent.tsx +172 -26
  44. package/src/transitions/easing.ts +25 -3
  45. package/src/transitions/index.ts +1 -0
  46. package/src/transitions/runtime.ts +63 -0
  47. package/src/transitions/spring.ts +1 -0
  48. package/src/types.ts +23 -1
@@ -67,6 +67,9 @@ const NUMERIC_TOP_LEVEL_KEYS = [
67
67
  'width',
68
68
  'height',
69
69
  'borderRadius',
70
+ 'shadowOpacity',
71
+ 'shadowRadius',
72
+ 'elevation',
70
73
  ] as const
71
74
 
72
75
  // Color-valued keys. Reanimated's value setter detects color strings and
@@ -83,8 +86,17 @@ const COLOR_KEYS = [
83
86
  'borderColor',
84
87
  'color',
85
88
  'tintColor',
89
+ 'shadowColor',
86
90
  ] as const
87
91
 
92
+ // Synthetic axes for the `shadowOffset: { width, height }` nested-object style
93
+ // prop. RN doesn't surface these as top-level keys; the worklet recomposes
94
+ // `shadowOffset` from the two synthetic SVs before emitting the style. The
95
+ // consumer's animate value (`shadowOffset: { width, height }`) decomposes into
96
+ // these at the activation / value-driving boundary; consumers don't write
97
+ // these keys directly.
98
+ const SHADOW_OFFSET_KEYS = ['shadowOffsetWidth', 'shadowOffsetHeight'] as const
99
+
88
100
  /**
89
101
  * Per-effect transform-group coordinator. Counts how many transform-axis
90
102
  * terminal callbacks are still pending; when the last one fires, the
@@ -97,12 +109,15 @@ const ALL_KEYS = [
97
109
  ...TRANSFORM_KEYS,
98
110
  ...NUMERIC_TOP_LEVEL_KEYS,
99
111
  ...COLOR_KEYS,
112
+ ...SHADOW_OFFSET_KEYS,
100
113
  ] as const
101
114
  type AnimatableKey = (typeof ALL_KEYS)[number]
102
115
  type TransformKey = (typeof TRANSFORM_KEYS)[number]
116
+ type ShadowOffsetKey = (typeof SHADOW_OFFSET_KEYS)[number]
103
117
 
104
118
  const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
105
119
  const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
120
+ const SHADOW_OFFSET_KEY_SET = new Set<AnimatableKey>(SHADOW_OFFSET_KEYS)
106
121
 
107
122
  const GESTURE_LAYER_NAMES = [
108
123
  'hovered',
@@ -131,6 +146,9 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
131
146
  width: 0,
132
147
  height: 0,
133
148
  borderRadius: 0,
149
+ shadowOpacity: 0,
150
+ shadowRadius: 0,
151
+ elevation: 0,
134
152
  // 'transparent' is the only safe universal default for colors: it works as
135
153
  // an initial seed for any color animation (no jarring opaque flash on mount
136
154
  // when `initial` is omitted) and rgba(0,0,0,0) interpolates cleanly into
@@ -139,6 +157,9 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
139
157
  borderColor: 'transparent',
140
158
  color: 'transparent',
141
159
  tintColor: 'transparent',
160
+ shadowColor: 'transparent',
161
+ shadowOffsetWidth: 0,
162
+ shadowOffsetHeight: 0,
142
163
  }
143
164
 
144
165
  function transitionFor<S>(
@@ -237,18 +258,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
237
258
  variantKey,
238
259
  )
239
260
 
240
- const animateRecord = (resolvedAnimate ?? {}) as Partial<
241
- Record<AnimatableKey, AnimatableValue<number | string>>
242
- >
261
+ const animateRecord = (resolvedAnimate ?? {}) as InternalAnimateRecord
243
262
  const initialRecord =
244
263
  initial && initial !== false
245
- ? (initial as Partial<Record<AnimatableKey, number | string>>)
264
+ ? (initial as InternalInitialRecord)
246
265
  : undefined
247
- const exitRecord = exit
248
- ? (exit as Partial<
249
- Record<AnimatableKey, AnimatableValue<number | string>>
250
- >)
251
- : undefined
266
+ const exitRecord = exit ? (exit as InternalAnimateRecord) : undefined
252
267
 
253
268
  // Gesture sub-state activation tracked as JS state. Activation flips drive
254
269
  // the per-layer progress shared values (0↔1); they intentionally do NOT
@@ -268,16 +283,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
268
283
  const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
269
284
  if (activeKeysRef.current === null) {
270
285
  const touched = new Set<AnimatableKey>()
271
- for (const k of ALL_KEYS) {
272
- if (k in animateRecord) touched.add(k)
273
- if (initialRecord && k in initialRecord) touched.add(k)
274
- }
286
+ collectTouchedKeys(touched, animateRecord)
287
+ if (initialRecord) collectTouchedKeys(touched, initialRecord)
275
288
  if (variants) {
276
289
  for (const variant of Object.values(variants) as object[]) {
277
290
  if (!variant) continue
278
- for (const k of ALL_KEYS) {
279
- if (k in variant) touched.add(k)
280
- }
291
+ collectTouchedKeys(touched, variant as Record<string, unknown>)
281
292
  }
282
293
  }
283
294
  if (gesture) {
@@ -288,23 +299,43 @@ export function createMotionComponent<C extends ComponentType<any>>(
288
299
  gesture.hovered,
289
300
  ] as Array<object | undefined>) {
290
301
  if (!subState) continue
291
- for (const k of ALL_KEYS) {
292
- if (k in subState) touched.add(k)
293
- }
294
- }
295
- }
296
- if (exitRecord) {
297
- for (const k of ALL_KEYS) {
298
- if (k in exitRecord) touched.add(k)
302
+ collectTouchedKeys(touched, subState as Record<string, unknown>)
299
303
  }
300
304
  }
305
+ if (exitRecord) collectTouchedKeys(touched, exitRecord)
301
306
  activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
302
307
  }
303
308
  const hasTransformRef = useRef<boolean>(
304
309
  activeKeysRef.current.some((k) => TRANSFORM_KEY_SET.has(k)),
305
310
  )
311
+ const hasShadowOffsetRef = useRef<boolean>(
312
+ activeKeysRef.current.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
313
+ )
306
314
 
307
315
  const sharedValues = useAnimatableSharedValues((key) => {
316
+ // Shadow offset synthetics seed from the corresponding axis on the
317
+ // `shadowOffset: { width, height }` source — the consumer doesn't write
318
+ // `shadowOffsetWidth` / `shadowOffsetHeight` directly. Fall back to the
319
+ // generic resting default when neither initial nor animate touched it.
320
+ if (SHADOW_OFFSET_KEY_SET.has(key)) {
321
+ const axis = shadowOffsetAxisFor(key as ShadowOffsetKey)
322
+ if (initial === false) {
323
+ return (
324
+ shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
325
+ DEFAULT_RESTING[key]
326
+ )
327
+ }
328
+ return (
329
+ shadowOffsetAxisValue(
330
+ initialRecord?.shadowOffset as
331
+ | { width?: number; height?: number }
332
+ | undefined,
333
+ axis,
334
+ ) ??
335
+ shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
336
+ DEFAULT_RESTING[key]
337
+ )
338
+ }
308
339
  if (initial === false) {
309
340
  const a = animateRecord[key]
310
341
  return restValue(a) ?? DEFAULT_RESTING[key]
@@ -398,7 +429,18 @@ export function createMotionComponent<C extends ComponentType<any>>(
398
429
  transformPending > 0 ? { remaining: transformPending } : undefined
399
430
 
400
431
  for (const key of ALL_KEYS) {
401
- const target = baseRecord[key]
432
+ // Shadow offset synthetics read their target from the nested
433
+ // `shadowOffset: { width, height }` source on `baseRecord` — the
434
+ // animate / exit record never has `shadowOffsetWidth` etc. on it
435
+ // directly. The synthetic transition follows the same `shadowOffset`
436
+ // top-level transition entry (no per-axis split).
437
+ const target: AnimatableValue<number | string> | undefined =
438
+ SHADOW_OFFSET_KEY_SET.has(key)
439
+ ? shadowOffsetAxisValue(
440
+ baseRecord.shadowOffset,
441
+ shadowOffsetAxisFor(key as ShadowOffsetKey),
442
+ )
443
+ : baseRecord[key]
402
444
  if (target === undefined) continue
403
445
  // Reduced-motion overrides every per-key transition (and any nested
404
446
  // sequence-step transition) with `no-animation`, which the resolver
@@ -407,7 +449,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
407
449
  // state" expectation.
408
450
  const cfg = shouldReduceMotion
409
451
  ? ({ type: 'no-animation' } as const)
410
- : transitionFor(key, transition)
452
+ : transitionFor(
453
+ SHADOW_OFFSET_KEY_SET.has(key)
454
+ ? ('shadowOffset' as keyof typeof baseRecord)
455
+ : key,
456
+ transition,
457
+ )
411
458
  if (isExiting) pending++
412
459
  const factory = makeKeyCallbackFactory(
413
460
  key,
@@ -487,8 +534,14 @@ export function createMotionComponent<C extends ComponentType<any>>(
487
534
  const animatedStyle = useAnimatedStyle(() => {
488
535
  const activeKeys = activeKeysRef.current!
489
536
  const hasTransform = hasTransformRef.current
537
+ const hasShadowOffset = hasShadowOffsetRef.current
490
538
  const out: Record<string, unknown> = {}
491
539
  const transform: Array<Record<string, unknown>> = []
540
+ // shadow-offset reassembly buffers. The two synthetic axis SVs feed in
541
+ // here and the recomposed `{ width, height }` object lands on `out`
542
+ // after the loop so RN gets a single `shadowOffset` style prop.
543
+ let shadowOffsetW = 0
544
+ let shadowOffsetH = 0
492
545
 
493
546
  // Read each progress SV exactly once so the chain below sees a coherent
494
547
  // snapshot for this frame. Reading them on the UI thread is cheap.
@@ -550,11 +603,18 @@ export function createMotionComponent<C extends ComponentType<any>>(
550
603
  transform.push(
551
604
  ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
552
605
  )
606
+ } else if (key === 'shadowOffsetWidth') {
607
+ shadowOffsetW = v as number
608
+ } else if (key === 'shadowOffsetHeight') {
609
+ shadowOffsetH = v as number
553
610
  } else {
554
611
  out[key] = v
555
612
  }
556
613
  }
557
614
  if (hasTransform) out.transform = transform
615
+ if (hasShadowOffset) {
616
+ out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
617
+ }
558
618
  return out
559
619
  })
560
620
 
@@ -643,12 +703,22 @@ function useAnimatableSharedValues(
643
703
  const width = useSharedValue<number | string>(init('width'))
644
704
  const height = useSharedValue<number | string>(init('height'))
645
705
  const borderRadius = useSharedValue<number | string>(init('borderRadius'))
706
+ const shadowOpacity = useSharedValue<number | string>(init('shadowOpacity'))
707
+ const shadowRadius = useSharedValue<number | string>(init('shadowRadius'))
708
+ const elevation = useSharedValue<number | string>(init('elevation'))
646
709
  const backgroundColor = useSharedValue<number | string>(
647
710
  init('backgroundColor'),
648
711
  )
649
712
  const borderColor = useSharedValue<number | string>(init('borderColor'))
650
713
  const color = useSharedValue<number | string>(init('color'))
651
714
  const tintColor = useSharedValue<number | string>(init('tintColor'))
715
+ const shadowColor = useSharedValue<number | string>(init('shadowColor'))
716
+ const shadowOffsetWidth = useSharedValue<number | string>(
717
+ init('shadowOffsetWidth'),
718
+ )
719
+ const shadowOffsetHeight = useSharedValue<number | string>(
720
+ init('shadowOffsetHeight'),
721
+ )
652
722
 
653
723
  const ref = useRef<SharedValueMap | null>(null)
654
724
  if (ref.current === null) {
@@ -665,10 +735,16 @@ function useAnimatableSharedValues(
665
735
  width,
666
736
  height,
667
737
  borderRadius,
738
+ shadowOpacity,
739
+ shadowRadius,
740
+ elevation,
668
741
  backgroundColor,
669
742
  borderColor,
670
743
  color,
671
744
  tintColor,
745
+ shadowColor,
746
+ shadowOffsetWidth,
747
+ shadowOffsetHeight,
672
748
  }
673
749
  }
674
750
  return ref.current
@@ -791,6 +867,65 @@ function makeKeyCallbackFactory(
791
867
  }
792
868
  }
793
869
 
870
+ /**
871
+ * Internal shape of the `animate` / `exit` record after the cast, widened to
872
+ * include the `shadowOffset: { width, height }` nested-object source. The
873
+ * nested object decomposes into the `shadowOffsetWidth` / `shadowOffsetHeight`
874
+ * synthetic axes downstream; consumers don't see the synthetics.
875
+ *
876
+ * v0.1 contract: `shadowOffset` accepts a single `{ width, height }` literal
877
+ * (no sequences, no `{ to }` step objects, no array keyframes). Sequence
878
+ * forms on the nested axes can land in v0.2 if real consumers ask for them.
879
+ */
880
+ type InternalAnimateRecord = Partial<
881
+ Record<AnimatableKey, AnimatableValue<number | string>>
882
+ > & {
883
+ shadowOffset?: { width?: number; height?: number }
884
+ }
885
+
886
+ type InternalInitialRecord = Partial<Record<AnimatableKey, number | string>> & {
887
+ shadowOffset?: { width?: number; height?: number }
888
+ }
889
+
890
+ /**
891
+ * Resolve a `shadowOffsetWidth` / `shadowOffsetHeight` synthetic key to the
892
+ * axis it represents on the nested-object source.
893
+ */
894
+ function shadowOffsetAxisFor(key: ShadowOffsetKey): 'width' | 'height' {
895
+ return key === 'shadowOffsetWidth' ? 'width' : 'height'
896
+ }
897
+
898
+ /**
899
+ * Read a single axis off a `shadowOffset: { width, height }` source. Returns
900
+ * `undefined` when the source is absent or the axis isn't set, so callers can
901
+ * fall back to the next source in the precedence chain.
902
+ */
903
+ function shadowOffsetAxisValue(
904
+ source: { width?: number; height?: number } | undefined,
905
+ axis: 'width' | 'height',
906
+ ): number | undefined {
907
+ return source?.[axis]
908
+ }
909
+
910
+ /**
911
+ * Populate `touched` with the `AnimatableKey`s mentioned in `record`. Direct
912
+ * matches (e.g. `opacity`, `width`) come from the key iteration; the nested
913
+ * `shadowOffset` source decomposes into the two `shadowOffset*` synthetics.
914
+ */
915
+ function collectTouchedKeys(
916
+ touched: Set<AnimatableKey>,
917
+ record: Record<string, unknown>,
918
+ ): void {
919
+ for (const k of ALL_KEYS) {
920
+ if (k in record) touched.add(k)
921
+ }
922
+ if ('shadowOffset' in record && record.shadowOffset) {
923
+ const so = record.shadowOffset as { width?: unknown; height?: unknown }
924
+ if (so.width !== undefined) touched.add('shadowOffsetWidth')
925
+ if (so.height !== undefined) touched.add('shadowOffsetHeight')
926
+ }
927
+ }
928
+
794
929
  /**
795
930
  * Number of sequence steps in an animatable value. `1` for plain values and
796
931
  * single-step `{ to }` objects; the array length for keyframe arrays.
@@ -930,6 +1065,17 @@ function resolveGestureLayers(
930
1065
  if (!subState) continue
931
1066
  const resolved: Record<string, number | string> = {}
932
1067
  for (const key of ALL_KEYS) {
1068
+ // Shadow offset synthetics decompose from the nested `shadowOffset:
1069
+ // { width, height }` source on the sub-state, the same as on `animate`.
1070
+ if (SHADOW_OFFSET_KEY_SET.has(key)) {
1071
+ const axis = shadowOffsetAxisFor(key as ShadowOffsetKey)
1072
+ const so = (
1073
+ subState as { shadowOffset?: { width?: number; height?: number } }
1074
+ ).shadowOffset
1075
+ const v = shadowOffsetAxisValue(so, axis)
1076
+ if (v !== undefined) resolved[key] = v
1077
+ continue
1078
+ }
933
1079
  const raw = (subState as Record<string, unknown>)[key]
934
1080
  if (raw === undefined) continue
935
1081
  const t = targetEndValue(raw as AnimatableValue<number | string>)
@@ -1,6 +1,7 @@
1
1
  // `isWorkletFunction` lives in `react-native-worklets` (the Reanimated 4 peer
2
2
  // dep); Reanimated's own re-export is deprecated.
3
3
  import { isWorkletFunction } from 'react-native-worklets'
4
+ import { type EasingInput } from '../types'
4
5
 
5
6
  /**
6
7
  * Reanimated 3.9+ validates that easing functions used in nested-transition
@@ -15,17 +16,38 @@ import { isWorkletFunction } from 'react-native-worklets'
15
16
  * the consumer's worklets babel plugin (the default Expo/RN setup), the
16
17
  * wrapper becomes a real worklet that captures the user fn via closure.
17
18
  *
19
+ * Reanimated 4 changed `Easing.bezier(...)` to return an
20
+ * `EasingFunctionFactory` (`{ factory: () => EasingFunction }`) rather than
21
+ * the function itself. The helper accepts both shapes — `EasingFunction` and
22
+ * `EasingFunctionFactory` — and unwraps the factory automatically so
23
+ * consumers don't have to call `.factory()` manually.
24
+ *
18
25
  * The user fn must be pure: no JS-thread captured refs, no shared mutable
19
26
  * state, no calls to non-worklet APIs.
20
27
  */
21
28
  export function ensureWorkletEasing(
22
- easing: ((t: number) => number) | undefined,
29
+ easing: EasingInput | undefined,
23
30
  ): ((t: number) => number) | undefined {
24
31
  if (!easing) return undefined
25
- if (isWorkletFunction(easing)) return easing
32
+ // Reanimated 4 `EasingFunctionFactory` — unwrap via `.factory()` before
33
+ // checking worklet status, so the wrapped fn (not the factory wrapper)
34
+ // ends up in the transition config.
35
+ const fn = isEasingFactory(easing) ? easing.factory() : easing
36
+ if (isWorkletFunction(fn)) return fn
26
37
  const wrapped = (t: number) => {
27
38
  'worklet'
28
- return easing(t)
39
+ return fn(t)
29
40
  }
30
41
  return wrapped
31
42
  }
43
+
44
+ function isEasingFactory(
45
+ value: EasingInput,
46
+ ): value is { factory: () => (t: number) => number } {
47
+ return (
48
+ typeof value === 'object' &&
49
+ value !== null &&
50
+ 'factory' in value &&
51
+ typeof (value as { factory: unknown }).factory === 'function'
52
+ )
53
+ }
@@ -1,5 +1,6 @@
1
1
  export { resolveTransition, resolveAnimatableValue } from './resolve'
2
2
  export { ensureWorkletEasing } from './easing'
3
3
  export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
4
+ export { buildReleaseAnimation } from './runtime'
4
5
  export { stableSig } from './sig'
5
6
  export { DEFAULT_SPRING, springToReanimated } from './spring'
@@ -0,0 +1,63 @@
1
+ import {
2
+ Easing,
3
+ withDecay,
4
+ withSpring,
5
+ withTiming,
6
+ } from 'react-native-reanimated'
7
+ import { springToReanimated } from './spring'
8
+ import { type TransitionConfig } from '../types'
9
+
10
+ const DEFAULT_TIMING_DURATION = 250
11
+
12
+ /**
13
+ * Worklet-safe single-step animation builder. Mirrors a subset of
14
+ * `resolveTransition` for the UI-thread path where the transition config is
15
+ * picked at gesture-release time, not at render time.
16
+ *
17
+ * Supported: spring / timing / decay / no-animation, single-step only.
18
+ * Not supported: sequences, top-level repeat, easing-function
19
+ * auto-worklet-wrapping (pass an already-worklet easing if you need a custom
20
+ * one — most release transitions don't).
21
+ *
22
+ * Use this from gesture worklets (`useDrag` / `usePan` release callbacks, or
23
+ * any custom `Gesture.Pan().onEnd(() => ...)` worklet) to animate a shared
24
+ * value with an Inertia transition without the JS round-trip that would lose
25
+ * the release velocity.
26
+ *
27
+ * For decay transitions, `toValue` is ignored — decay decelerates from the
28
+ * SV's current position via its own physics. Pass `0` if you don't have one.
29
+ */
30
+ export function buildReleaseAnimation(
31
+ transition: TransitionConfig,
32
+ toValue: number,
33
+ ): unknown {
34
+ 'worklet'
35
+ if (transition.type === 'no-animation') return toValue
36
+ if (transition.type === 'decay') {
37
+ const cfg: {
38
+ velocity: number
39
+ deceleration?: number
40
+ clamp?: [number, number]
41
+ } = { velocity: transition.velocity ?? 0 }
42
+ if (transition.deceleration !== undefined) {
43
+ cfg.deceleration = transition.deceleration
44
+ }
45
+ if (transition.clamp !== undefined) cfg.clamp = transition.clamp
46
+ return withDecay(cfg)
47
+ }
48
+ if (transition.type === 'timing') {
49
+ // Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
50
+ // rather than the function itself. Unwrap inline so consumers calling
51
+ // `buildReleaseAnimation` from a gesture worklet don't have to.
52
+ const e = transition.easing
53
+ const easingFn =
54
+ e && typeof e === 'object' && 'factory' in e
55
+ ? e.factory()
56
+ : (e ?? Easing.inOut(Easing.ease))
57
+ return withTiming(toValue, {
58
+ duration: transition.duration ?? DEFAULT_TIMING_DURATION,
59
+ easing: easingFn,
60
+ })
61
+ }
62
+ return withSpring(toValue, springToReanimated(transition))
63
+ }
@@ -30,6 +30,7 @@ export const DEFAULT_SPRING: Required<
30
30
  * what designers and prior-art consumers expect.
31
31
  */
32
32
  export function springToReanimated(t: SpringTransition) {
33
+ 'worklet'
33
34
  return {
34
35
  stiffness: t.tension ?? DEFAULT_SPRING.tension,
35
36
  damping: t.friction ?? DEFAULT_SPRING.friction,
package/src/types.ts CHANGED
@@ -32,10 +32,24 @@ export interface SpringTransition {
32
32
  repeat?: RepeatConfig
33
33
  }
34
34
 
35
+ /**
36
+ * Easing input accepted by `TimingTransition`. Either a plain easing function
37
+ * `(t: number) => number` (the pre-Reanimated-4 shape and the shape consumers
38
+ * author by hand) or an `EasingFunctionFactory` (the Reanimated 4 shape
39
+ * returned by `Easing.bezier(...)` and similar builders). The resolver
40
+ * unwraps the factory automatically — consumers don't have to call
41
+ * `.factory()` themselves.
42
+ */
43
+ export type EasingFunction = (t: number) => number
44
+ export interface EasingFunctionFactory {
45
+ factory: () => EasingFunction
46
+ }
47
+ export type EasingInput = EasingFunction | EasingFunctionFactory
48
+
35
49
  export interface TimingTransition {
36
50
  type: 'timing'
37
51
  duration?: number
38
- easing?: (t: number) => number
52
+ easing?: EasingInput
39
53
  delay?: number
40
54
  repeat?: RepeatConfig
41
55
  }
@@ -192,6 +206,14 @@ export type VariantsMap<C> = Record<string, AnimateStyle<C>>
192
206
  * Configure per-layer fade timing via `transition.<stateName>` on the parent
193
207
  * primitive (see `GestureLayerTransitions`); without it, layers default to
194
208
  * the parent transition or the library default spring.
209
+ *
210
+ * **Priority cascade is the only composition mode on this prop.** Non-priority
211
+ * blends — clamped-max (`Math.max(hover*α, focus*β, press*γ)`, as used by MD3
212
+ * state-layer haloes), additive accumulation, or any per-key custom blend —
213
+ * are not expressible declaratively. Drop to `useGesture()` for those: it
214
+ * returns the four progress shared values and a handler bag, and you write
215
+ * a `useAnimatedStyle` block with whatever composition you need. The hook's
216
+ * JSDoc shows the clamped-max halo pattern in full.
195
217
  */
196
218
  export interface GestureSubStates<C> {
197
219
  pressed?: AnimateStyle<C>