react-native-smooth-clip-view 0.2.8 → 0.3.0

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 (132) hide show
  1. package/README.md +86 -21
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +14 -2
  4. package/android/proguard-rules.pro +3 -3
  5. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  6. package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
  7. package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
  8. package/android/src/main/cpp/SmoothClipTrace.h +25 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  14. package/cpp/SmoothClipAnimationCurve.h +151 -122
  15. package/cpp/SmoothClipRegistry.h +102 -4
  16. package/cpp/SmoothClipSharedGeometry.h +146 -7
  17. package/cpp/SmoothClipVelocityTracker.h +13 -9
  18. package/ios/SmoothClipGeometry.h +184 -6
  19. package/ios/SmoothClipRegistry.mm +1033 -25
  20. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  21. package/ios/SmoothClipTurboModule.cpp +857 -2
  22. package/ios/SmoothClipTurboModule.h +310 -0
  23. package/ios/SmoothClipView.mm +714 -183
  24. package/ios/SmoothClipViewRegistry.h +1 -0
  25. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  26. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  27. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  28. package/lib/module/SmoothClipView.ios.js +22 -6
  29. package/lib/module/SmoothClipView.ios.js.map +1 -1
  30. package/lib/module/SmoothClipView.js +1 -46
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  33. package/lib/module/capabilities.js +4 -0
  34. package/lib/module/capabilities.js.map +1 -0
  35. package/lib/module/capabilities.native.js +36 -0
  36. package/lib/module/capabilities.native.js.map +1 -0
  37. package/lib/module/capabilityTypes.js +11 -0
  38. package/lib/module/capabilityTypes.js.map +1 -0
  39. package/lib/module/driverState.js +0 -28
  40. package/lib/module/driverState.js.map +1 -1
  41. package/lib/module/drivers.js +1 -281
  42. package/lib/module/drivers.js.map +1 -1
  43. package/lib/module/drivers.native.js +340 -75
  44. package/lib/module/drivers.native.js.map +1 -1
  45. package/lib/module/geometry.js +147 -17
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/groupDriverTypes.js +4 -0
  48. package/lib/module/groupDriverTypes.js.map +1 -0
  49. package/lib/module/groupDrivers.android.js +4 -0
  50. package/lib/module/groupDrivers.android.js.map +1 -0
  51. package/lib/module/groupDrivers.ios.js +4 -0
  52. package/lib/module/groupDrivers.ios.js.map +1 -0
  53. package/lib/module/groupDrivers.js +4 -0
  54. package/lib/module/groupDrivers.js.map +1 -0
  55. package/lib/module/groupDrivers.native.js +799 -0
  56. package/lib/module/groupDrivers.native.js.map +1 -0
  57. package/lib/module/index.js +3 -2
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/presentationProtocol.js +15 -0
  60. package/lib/module/presentationProtocol.js.map +1 -0
  61. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  62. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  67. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  68. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  69. package/lib/typescript/src/capabilities.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  72. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  73. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  74. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/driverState.d.ts +1 -6
  76. package/lib/typescript/src/driverState.d.ts.map +1 -1
  77. package/lib/typescript/src/driverTypes.d.ts +39 -11
  78. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.d.ts +2 -4
  80. package/lib/typescript/src/drivers.d.ts.map +1 -1
  81. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  82. package/lib/typescript/src/geometry.d.ts +36 -2
  83. package/lib/typescript/src/geometry.d.ts.map +1 -1
  84. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  85. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  91. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  92. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  93. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  94. package/lib/typescript/src/index.d.ts +6 -3
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  97. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  98. package/package.json +10 -11
  99. package/src/NativeSmoothClipModule.ts +275 -0
  100. package/src/SmoothClipView.ios.tsx +30 -11
  101. package/src/SmoothClipView.tsx +2 -76
  102. package/src/SmoothClipViewNativeComponent.ts +27 -1
  103. package/src/capabilities.native.ts +69 -0
  104. package/src/capabilities.ts +2 -0
  105. package/src/capabilityTypes.ts +17 -0
  106. package/src/driverState.ts +1 -41
  107. package/src/driverTypes.ts +56 -11
  108. package/src/drivers.native.ts +921 -151
  109. package/src/drivers.ts +2 -423
  110. package/src/geometry.ts +231 -27
  111. package/src/groupDriverTypes.ts +117 -0
  112. package/src/groupDrivers.android.ts +1 -0
  113. package/src/groupDrivers.ios.ts +1 -0
  114. package/src/groupDrivers.native.ts +1189 -0
  115. package/src/groupDrivers.ts +1 -0
  116. package/src/index.ts +30 -2
  117. package/src/presentationProtocol.ts +28 -0
  118. package/lib/module/SmoothClipView.web.js +0 -54
  119. package/lib/module/SmoothClipView.web.js.map +0 -1
  120. package/lib/module/index.web.js +0 -8
  121. package/lib/module/index.web.js.map +0 -1
  122. package/lib/module/webClipPath.js +0 -25
  123. package/lib/module/webClipPath.js.map +0 -1
  124. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  125. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  126. package/lib/typescript/src/index.web.d.ts +0 -9
  127. package/lib/typescript/src/index.web.d.ts.map +0 -1
  128. package/lib/typescript/src/webClipPath.d.ts +0 -7
  129. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  130. package/src/SmoothClipView.web.tsx +0 -66
  131. package/src/index.web.ts +0 -23
  132. package/src/webClipPath.ts +0 -30
@@ -1,20 +1,37 @@
1
1
  "use strict";
2
2
 
3
- // The shared NATIVE driver: iOS and Android both execute this file, via the
3
+ // The shared native driver: iOS and Android both execute this file, via the
4
4
  // one-line re-exports in drivers.ios.ts / drivers.android.ts that Metro's
5
5
  // platform resolution picks up from index.ts's `./drivers` import. Only the
6
- // `./smoothClipNative` import below is platform-split. drivers.ts (no
7
- // suffix) is the web/Reanimated fallback, not this driver's sibling.
6
+ // `./smoothClipNative` import below is platform-split.
8
7
  import { useEffect, useRef } from 'react';
8
+ import { Platform } from 'react-native';
9
9
  import { useSharedValue } from 'react-native-reanimated';
10
10
  import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
11
11
  import NativeSmoothClipModule from './smoothClipNative';
12
+ import { getSmoothClipCapabilities } from './capabilities';
12
13
  import { allocateDriverId, attachDriverState, createDriverState, detachDriverState, getDriverState, setDriverState } from "./driverState.js";
13
14
  import { completeNativeAnimation, synchronizeNativeCompletion } from "./nativeCompletion.js";
14
- import { clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
15
+ import { canonicalizeClipPresentation, clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
15
16
  import { createReactRequest, rejectDriverRequests, resolveReactRequest } from "./reactRequests.js";
16
17
  const INTERACTIVE = 0;
17
18
  const NATIVE = 1;
19
+
20
+ // Only Android consumes the Reanimated start stamp (its frame-clock anchor —
21
+ // docs/android-frame-clock-anchor.md). iOS's TurboModule reads its declared
22
+ // parameters positionally and drops the trailing argument, so capturing the
23
+ // stamp there is a wasted native _getAnimationTimestamp() call per animateTo.
24
+ // Resolved once at module scope; the worklet captures the primitive.
25
+ const NEEDS_START_STAMP = Platform.OS === 'android';
26
+ const PRESENTATION_PROTOCOL_VERSION = getSmoothClipCapabilities().presentationProtocolVersion;
27
+ const NativeSmoothClipModuleV2 = NativeSmoothClipModule;
28
+
29
+ // Widened like the animate* functions below: one trailing argument beyond the
30
+ // TurboModule spec — whether this write should record an `'inherit'` velocity
31
+ // sample. Android's JSI bindings read it (absent means record, the pre-flag
32
+ // behavior); the iOS TurboModule reads its declared parameters positionally
33
+ // and drops it, so iOS always records.
34
+
18
35
  const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
19
36
  const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
20
37
 
@@ -31,15 +48,27 @@ const animateKeyframesHostFunction = NativeSmoothClipModule.animateKeyframes;
31
48
  const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
32
49
  const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
33
50
  const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
51
+ const setPresentationV2HostFunction = NativeSmoothClipModuleV2.setClipPresentationV2;
52
+ const beginInteractionV2HostFunction = NativeSmoothClipModuleV2.beginInteractionV2;
53
+ const snapshotCurrentV2HostFunction = NativeSmoothClipModuleV2.snapshotCurrentV2;
54
+ const animateTimingV2HostFunction = NativeSmoothClipModuleV2.animateTimingV2;
55
+ const animateSpringV2HostFunction = NativeSmoothClipModuleV2.animateSpringV2;
56
+ const animateKeyframesV2HostFunction = NativeSmoothClipModuleV2.animateKeyframesV2;
57
+ const animateTimingFromV2HostFunction = NativeSmoothClipModuleV2.animateTimingFromV2;
58
+ const animateSpringFromV2HostFunction = NativeSmoothClipModuleV2.animateSpringFromV2;
59
+ const animateKeyframesFromV2HostFunction = NativeSmoothClipModuleV2.animateKeyframesFromV2;
60
+ const cancelAnimationV2HostFunction = NativeSmoothClipModuleV2.cancelAnimationV2;
34
61
 
35
- // Workaround for react-native-worklets 0.10: the RN-side serialization cache
62
+ // Workaround for react-native-worklets 0.10.0 (fixed upstream in >=0.10.1,
63
+ // which drops the registry/proxy design; kept while the peer range still
64
+ // allows 0.10.0 consumers): the RN-side serialization cache
36
65
  // hands out one remote-function registry id per function forever, but the
37
66
  // registry entry is deleted as soon as any transient UI-side proxy of that
38
67
  // function is garbage collected. A later scheduleOnRN for the same function
39
68
  // then reads `undefined` from the registry and aborts in Value::getObject.
40
69
  // Pinning one UI-side reference for the app lifetime keeps the proxy — and
41
- // with it the registry entry — alive. Remove once worklets decouples registry
42
- // cleanup from proxy lifetime.
70
+ // with it the registry entry — alive. Remove once the peer range requires
71
+ // worklets >=0.10.1.
43
72
  scheduleOnUI(resolver => {
44
73
  'worklet';
45
74
 
@@ -55,7 +84,7 @@ function reduceMotionCode(value) {
55
84
  return 0;
56
85
  }
57
86
  }
58
- function animationIsFinite(target, animation) {
87
+ function animationIsFinite(animation, strictV2) {
59
88
  'worklet';
60
89
 
61
90
  if (animation.from !== undefined && !isFiniteClipPresentation(animation.from)) {
@@ -65,34 +94,53 @@ function animationIsFinite(target, animation) {
65
94
  }
66
95
  if (animation.type === 'timing') {
67
96
  const [x1,, x2] = animation.controlPoints;
68
- return Number.isFinite(animation.duration) && animation.controlPoints.every(Number.isFinite) &&
97
+ return Number.isFinite(animation.duration) && (!strictV2 || animation.duration >= 0) && animation.controlPoints.every(Number.isFinite) && (
69
98
  // A cubic-bezier easing is only defined for x within [0,1] (the CSS /
70
99
  // CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
71
100
  // makes the Android parameter solve meaningless while CoreAnimation
72
101
  // silently clamps — reject like any other invalid input instead of
73
102
  // diverging per platform.
74
- x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1;
103
+ !strictV2 || x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1);
75
104
  }
76
105
  if (animation.type === 'spring') {
77
- return [animation.mass ?? 1, animation.stiffness ?? 100, animation.damping ?? 10, animation.initialVelocity === 'inherit' ? 0 : animation.initialVelocity ?? 0].every(Number.isFinite);
78
- }
79
- if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
80
- return false;
81
- }
82
- let previousOffset = -1;
83
- for (const frame of animation.frames) {
84
- if (!Number.isFinite(frame.offset) || frame.offset < 0 || frame.offset > 1 || frame.offset <= previousOffset || !isFiniteClipPresentation(frame.presentation)) {
85
- return false;
86
- }
87
- previousOffset = frame.offset;
106
+ const mass = animation.mass ?? 1;
107
+ const stiffness = animation.stiffness ?? 100;
108
+ const damping = animation.damping ?? 10;
109
+ const initialVelocity = animation.initialVelocity === 'inherit' ? 0 : animation.initialVelocity ?? 0;
110
+ return [mass, stiffness, damping, initialVelocity].every(Number.isFinite) && (!strictV2 || mass > 0 && stiffness > 0 && damping >= 0);
88
111
  }
89
- return animation.frames[0]?.offset === 0 && previousOffset === 1 && clipPresentationEquals(animation.frames[animation.frames.length - 1]?.presentation ?? null, target);
112
+ // Keyframe frames validate inside flattenFiniteKeyframes — one traversal
113
+ // both checks and flattens, instead of walking every frame twice. The type
114
+ // check is statically always-true for the TS union but load-bearing for JS
115
+ // callers: an unrecognized type must reject here, or it would skip the
116
+ // keyframe gate entirely and hand native a null frames array.
117
+ return animation.type === 'keyframes' && (!strictV2 || animation.duration >= 0);
90
118
  }
91
119
  function presentationFromNative(values, fallback, offset = 0) {
92
120
  'worklet';
93
121
 
122
+ if (values.length >= offset + 12) {
123
+ const presentation = canonicalizeClipPresentation({
124
+ clip: {
125
+ x: values[offset],
126
+ y: values[offset + 1],
127
+ width: values[offset + 2],
128
+ height: values[offset + 3],
129
+ radius: 0,
130
+ topLeftRadius: values[offset + 4],
131
+ topRightRadius: values[offset + 5],
132
+ bottomRightRadius: values[offset + 6],
133
+ bottomLeftRadius: values[offset + 7],
134
+ curve: values[offset + 8] === 1 ? 'continuous' : 'circular'
135
+ },
136
+ contentTranslateX: values[offset + 9],
137
+ contentTranslateY: values[offset + 10],
138
+ contentScale: values[offset + 11]
139
+ });
140
+ return presentation ?? fallback;
141
+ }
94
142
  if (values.length < offset + 7) return fallback;
95
- const presentation = {
143
+ const presentation = canonicalizeClipPresentation({
96
144
  clip: {
97
145
  x: values[offset],
98
146
  y: values[offset + 1],
@@ -102,20 +150,83 @@ function presentationFromNative(values, fallback, offset = 0) {
102
150
  },
103
151
  contentTranslateX: values[offset + 5],
104
152
  contentTranslateY: values[offset + 6]
105
- };
106
- return isFiniteClipPresentation(presentation) ? presentation : fallback;
153
+ });
154
+ return presentation ?? fallback;
155
+ }
156
+ function curveCode(presentation) {
157
+ 'worklet';
158
+
159
+ return presentation.clip.curve === 'continuous' ? 1 : 0;
160
+ }
161
+ function isV1Compatible(presentation) {
162
+ 'worklet';
163
+
164
+ const clip = presentation.clip;
165
+ return clip.curve === 'circular' && clip.topLeftRadius === clip.radius && clip.topRightRadius === clip.radius && clip.bottomRightRadius === clip.radius && clip.bottomLeftRadius === clip.radius && presentation.contentScale === 1;
107
166
  }
108
- function flattenedKeyframes(animation) {
167
+ function springScaleIsProvablyPositive(start, target, animation) {
109
168
  'worklet';
110
169
 
170
+ if (start.contentScale === target.contentScale) return true;
171
+
172
+ // A zero-velocity critically/over-damped step is monotonic, so its sampled
173
+ // scale remains between two already-positive endpoints. Inherited or
174
+ // under-damped configurations can overshoot; callers must compile those
175
+ // plans to exact keyframes instead of risking a non-positive transform.
176
+ const mass = animation.mass ?? 1;
177
+ const stiffness = animation.stiffness ?? 100;
178
+ const damping = animation.damping ?? 10;
179
+ return animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness;
180
+ }
181
+
182
+ // Validates and flattens the keyframe list in one traversal (the flat number
183
+ // array is the wire format the native side reads back positionally). Returns
184
+ // null when any frame is invalid — same rejection contract the two-pass
185
+ // validate-then-flatten had.
186
+ function flattenFiniteKeyframes(target, animation) {
187
+ 'worklet';
188
+
189
+ if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
190
+ return null;
191
+ }
111
192
  const values = [];
193
+ const canonicalFrames = [];
194
+ let previousOffset = -1;
112
195
  for (const frame of animation.frames) {
196
+ const {
197
+ offset
198
+ } = frame;
199
+ const presentation = canonicalizeClipPresentation(frame.presentation);
200
+ if (!Number.isFinite(offset) || offset < 0 || offset > 1 || offset <= previousOffset || presentation === null || presentation.clip.curve !== target.clip.curve) {
201
+ return null;
202
+ }
203
+ previousOffset = offset;
204
+ canonicalFrames.push(presentation);
113
205
  const {
114
206
  clip,
115
207
  contentTranslateX,
116
- contentTranslateY
117
- } = frame.presentation;
118
- values.push(frame.offset, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY);
208
+ contentTranslateY,
209
+ contentScale
210
+ } = presentation;
211
+ values.push(offset, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(presentation), contentTranslateX, contentTranslateY, contentScale);
212
+ }
213
+ if (animation.frames[0]?.offset !== 0 || previousOffset !== 1 || !clipPresentationEquals(canonicalFrames[canonicalFrames.length - 1] ?? null, target)) {
214
+ return null;
215
+ }
216
+ return {
217
+ frames: values,
218
+ canonicalFrames
219
+ };
220
+ }
221
+ function flattenV1Keyframes(frames, offsets) {
222
+ 'worklet';
223
+
224
+ const values = [];
225
+ for (let index = 0; index < frames.length; index += 1) {
226
+ const presentation = frames[index];
227
+ const offset = offsets[index];
228
+ if (presentation === undefined || offset === undefined) continue;
229
+ values.push(offset, presentation.clip.x, presentation.clip.y, presentation.clip.width, presentation.clip.height, presentation.clip.radius, presentation.contentTranslateX, presentation.contentTranslateY);
119
230
  }
120
231
  return values;
121
232
  }
@@ -125,7 +236,11 @@ function uiOnly() {
125
236
  throw new Error('[SmoothClipView] driver.ui methods must run on the UI runtime. Use driver.react from React code.');
126
237
  }
127
238
  export function useSmoothClipDriver(initialValue, options = {}) {
128
- const initialPresentation = 'clip' in initialValue ? initialValue : createClipPresentation(initialValue);
239
+ const requestedInitialPresentation = 'clip' in initialValue ? initialValue : createClipPresentation(initialValue);
240
+ const initialPresentation = canonicalizeClipPresentation(requestedInitialPresentation);
241
+ if (initialPresentation === null) {
242
+ throw new Error('[SmoothClipView] Initial presentation must be finite.');
243
+ }
129
244
  const presentation = useSharedValue(initialPresentation);
130
245
  const ownership = useSharedValue(INTERACTIVE);
131
246
  const suppressDeliveries = useSharedValue(0);
@@ -148,6 +263,11 @@ export function useSmoothClipDriver(initialValue, options = {}) {
148
263
  if (driverRef.current === null) {
149
264
  const driverId = allocateDriverId();
150
265
  const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
266
+ // Captured once, like reduceMotion. Only the per-frame setScalars stream
267
+ // consults it; set(), the fused animateTo `from` seed, and the effect's
268
+ // declarative deliveries always record, so every once-per-call channel
269
+ // keeps its pre-flag 'inherit' behavior.
270
+ const velocityTracking = options.velocityTracking === true;
151
271
  const seedPresentation = next => {
152
272
  'worklet';
153
273
 
@@ -165,8 +285,9 @@ export function useSmoothClipDriver(initialValue, options = {}) {
165
285
  const beginOnUI = () => {
166
286
  'worklet';
167
287
 
168
- if (disposed.value !== 0) return presentation.value;
169
- const current = presentationFromNative(beginInteractionHostFunction(driverId), presentation.value);
288
+ const fallback = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
289
+ if (disposed.value !== 0) return fallback;
290
+ const current = presentationFromNative(PRESENTATION_PROTOCOL_VERSION === 2 && beginInteractionV2HostFunction ? beginInteractionV2HostFunction(driverId) : beginInteractionHostFunction(driverId), fallback);
170
291
  activeAnimationId.value = 0;
171
292
  ownership.value = INTERACTIVE;
172
293
  scalarsStale.value = 0;
@@ -176,28 +297,72 @@ export function useSmoothClipDriver(initialValue, options = {}) {
176
297
  const setOnUI = next => {
177
298
  'worklet';
178
299
 
179
- if (disposed.value !== 0 || !isFiniteClipPresentation(next)) return;
300
+ const canonical = canonicalizeClipPresentation(next);
301
+ if (disposed.value !== 0 || canonical === null) return;
180
302
  activeAnimationId.value = 0;
181
303
  ownership.value = INTERACTIVE;
182
304
  scalarsStale.value = 0;
183
305
  const {
184
306
  clip,
185
307
  contentTranslateX,
186
- contentTranslateY
187
- } = next;
188
- setPresentationHostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, true, false);
189
- seedPresentation(next);
308
+ contentTranslateY,
309
+ contentScale
310
+ } = canonical;
311
+ // Always records: only the per-frame setScalars hot path is gated by
312
+ // velocityTracking. set() already pays full validation and a SharedValue
313
+ // seed per call, so the sample recording is noise here.
314
+ if (PRESENTATION_PROTOCOL_VERSION === 2 && setPresentationV2HostFunction) {
315
+ setPresentationV2HostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(canonical), contentTranslateX, contentTranslateY, contentScale, true, false, true);
316
+ } else if (isV1Compatible(canonical)) {
317
+ setPresentationHostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, true, false);
318
+ } else {
319
+ return;
320
+ }
321
+ seedPresentation(canonical);
190
322
  };
191
- const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY, overridePendingAnimation = false) => {
323
+ const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY, overridePendingAnimation = false, forceRecordVelocity = false) => {
192
324
  'worklet';
193
325
 
326
+ // Resolved in the body, not as a default-parameter initializer: the
327
+ // worklets Babel plugin only captures closure variables referenced
328
+ // inside the function body, so `= velocityTracking` up in the parameter
329
+ // list would throw "Property 'velocityTracking' doesn't exist" on the
330
+ // UI runtime.
331
+ const recordVelocity = forceRecordVelocity || velocityTracking;
332
+ // Number.isFinite (not a fused arithmetic gate): it also rejects
333
+ // non-number types, which arithmetic would coerce past the gate and
334
+ // into a UI-runtime throw from the binding's asNumber. The gate must
335
+ // stay on this side of the bridge: the native binding drops non-finite
336
+ // writes silently, and flipping the ownership SharedValues below for a
337
+ // write native never applied would let the deliver listener record
338
+ // values native has not observed — a later identical declarative write
339
+ // then dedupes against them and is swallowed for good.
194
340
  if (disposed.value !== 0 || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(radius) || !Number.isFinite(contentTranslateX) || !Number.isFinite(contentTranslateY)) {
195
341
  return;
196
342
  }
197
343
  if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
198
344
  if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
199
345
  if (scalarsStale.value === 0) scalarsStale.value = 1;
200
- setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true, overridePendingAnimation);
346
+ setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true, overridePendingAnimation, recordVelocity);
347
+ };
348
+ const setPresentationScalarsOnUI = (x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale, overridePendingAnimation = false, forceRecordVelocity = false) => {
349
+ 'worklet';
350
+
351
+ const values = [x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale];
352
+ if (disposed.value !== 0 || !values.every(Number.isFinite) || nextCurveCode !== 0 && nextCurveCode !== 1 || contentScale <= 0) {
353
+ return;
354
+ }
355
+ const recordVelocity = forceRecordVelocity || velocityTracking;
356
+ if (PRESENTATION_PROTOCOL_VERSION !== 2 || !setPresentationV2HostFunction) {
357
+ if (nextCurveCode === 0 && contentScale === 1 && topLeftRadius === topRightRadius && topLeftRadius === bottomRightRadius && topLeftRadius === bottomLeftRadius) {
358
+ setScalarsOnUI(x, y, width, height, topLeftRadius, contentTranslateX, contentTranslateY, overridePendingAnimation, forceRecordVelocity);
359
+ }
360
+ return;
361
+ }
362
+ if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
363
+ if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
364
+ if (scalarsStale.value === 0) scalarsStale.value = 1;
365
+ setPresentationV2HostFunction(driverId, x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale, true, overridePendingAnimation, recordVelocity);
201
366
  };
202
367
  const animateOnUI = (target, animation) => {
203
368
  'worklet';
@@ -206,7 +371,43 @@ export function useSmoothClipDriver(initialValue, options = {}) {
206
371
  // invalid stale call cannot mint a native rejection id/completion against
207
372
  // a driver whose listener and registry entry are already gone.
208
373
  if (disposed.value !== 0) return 0;
209
- if (!isFiniteClipPresentation(target) || !animationIsFinite(target, animation)) {
374
+ const canonicalTarget = canonicalizeClipPresentation(target);
375
+ const canonicalFrom = animation.from === undefined ? undefined : canonicalizeClipPresentation(animation.from);
376
+ const keyframeResult = animation.type === 'keyframes' ? canonicalTarget === null ? null : flattenFiniteKeyframes(canonicalTarget, animation) : null;
377
+ if (canonicalTarget === null || animation.from !== undefined && canonicalFrom === null || animation.type === 'keyframes' && keyframeResult === null) {
378
+ return rejectAnimationHostFunction(driverId);
379
+ }
380
+ const resolvedFrom = canonicalFrom ?? undefined;
381
+ const v1Compatible = isV1Compatible(canonicalTarget) && (resolvedFrom === undefined || isV1Compatible(resolvedFrom)) && (animation.type !== 'keyframes' || (keyframeResult?.canonicalFrames.every(isV1Compatible) ?? false));
382
+ // V1-representable plans deliberately stay on the original seven-scalar
383
+ // bridge even when V2 is installed. Besides preserving bridge shape,
384
+ // this retains V1's finite-value/clamping and host-normalization rules.
385
+ const useV2 = !v1Compatible;
386
+ if (!animationIsFinite(animation, useV2)) {
387
+ return rejectAnimationHostFunction(driverId);
388
+ }
389
+ const hasV2Protocol = PRESENTATION_PROTOCOL_VERSION === 2 && snapshotCurrentV2HostFunction !== undefined && animateTimingV2HostFunction !== undefined && animateSpringV2HostFunction !== undefined && animateKeyframesV2HostFunction !== undefined;
390
+ const hasAtomicFromMethod = resolvedFrom === undefined || (animation.type === 'timing' ? animateTimingFromV2HostFunction !== undefined : animation.type === 'spring' ? animateSpringFromV2HostFunction !== undefined : animateKeyframesFromV2HostFunction !== undefined);
391
+ // This is intentionally before sampling or any ownership/group/
392
+ // presentation mutation. New JS against old native must fail closed,
393
+ // and an explicit V2 start must never degrade to set()+animate().
394
+ if (useV2 && (!hasV2Protocol || !hasAtomicFromMethod)) {
395
+ return rejectAnimationHostFunction(driverId);
396
+ }
397
+ if (useV2 && resolvedFrom !== undefined && animation.type === 'keyframes' && !clipPresentationEquals(keyframeResult?.canonicalFrames[0] ?? null, resolvedFrom)) {
398
+ return rejectAnimationHostFunction(driverId);
399
+ }
400
+ const currentPresentation = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
401
+ const sampledStart = resolvedFrom ?? (useV2 ? presentationFromNative(snapshotCurrentV2HostFunction(driverId), currentPresentation) : currentPresentation);
402
+ // setPresentationScalars intentionally leaves the public SharedValue
403
+ // stale to keep the hot path allocation-free. Validate an implicit
404
+ // retarget against the sampled native-visible start, not that stale JS
405
+ // value; otherwise a streamed curve transition cannot settle natively on
406
+ // its new (now stable) curve.
407
+ if (canonicalTarget.clip.curve !== sampledStart.clip.curve) {
408
+ return rejectAnimationHostFunction(driverId);
409
+ }
410
+ if (useV2 && animation.type === 'spring' && !springScaleIsProvablyPositive(sampledStart, canonicalTarget, animation)) {
210
411
  return rejectAnimationHostFunction(driverId);
211
412
  }
212
413
  // Past the hook's cleanup the native entry is gone for good, and an
@@ -215,14 +416,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
215
416
  // rejectAnimation, which would mint an id and a completion for a driver
216
417
  // whose JS state is already detached. This is the "unsupported dispatch"
217
418
  // arm of the documented 0 contract.
218
- const from = animation.from;
219
- if (from !== undefined) {
419
+ const from = resolvedFrom;
420
+ if (from !== undefined && !useV2) {
220
421
  // Fused hot write: exactly setScalars(from…) issued immediately
221
422
  // before the handoff, so native's latest value — the animation
222
423
  // start below — is the caller's explicit presentation. Also
223
424
  // re-grabs from a running animation, which the implicit
224
- // interactive-start path would silently skip.
225
- setScalarsOnUI(from.clip.x, from.clip.y, from.clip.width, from.clip.height, from.clip.radius, from.contentTranslateX, from.contentTranslateY, true);
425
+ // interactive-start path would silently skip. Always records a
426
+ // velocity sample regardless of velocityTracking: this is one write
427
+ // per animateTo, not the per-frame stream the flag exists to keep
428
+ // cheap, and an unrecorded seed would invalidate the history a
429
+ // set()-driven drag just recorded (the tracker's coalesce and
430
+ // identical-re-record rules were designed for exactly this write).
431
+ setScalarsOnUI(from.clip.x, from.clip.y, from.clip.width, from.clip.height, from.clip.radius, from.contentTranslateX, from.contentTranslateY, true, true);
226
432
  }
227
433
 
228
434
  // After a setScalars hot write the SharedValue no longer matches what
@@ -231,12 +437,17 @@ export function useSmoothClipDriver(initialValue, options = {}) {
231
437
  // latest value instead.
232
438
  const scalarsWereStale = scalarsStale.value !== 0;
233
439
  const hasInteractiveStart = ownership.value === INTERACTIVE && !scalarsWereStale;
234
- const start = presentation.value;
235
- ownership.value = NATIVE;
236
- presentation.value = target;
237
- scalarsStale.value = 0;
238
- const startClip = start.clip;
239
- const targetClip = target.clip;
440
+ const start = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
441
+ // V1 preserves the old optimistic SharedValue ownership transition.
442
+ // V2 commits it only after native has atomically accepted the plan.
443
+ if (!useV2) {
444
+ ownership.value = NATIVE;
445
+ presentation.value = canonicalTarget;
446
+ scalarsStale.value = 0;
447
+ }
448
+ const dispatchStart = useV2 && from !== undefined ? from : start;
449
+ const startClip = dispatchStart.clip;
450
+ const targetClip = canonicalTarget.clip;
240
451
  // Reanimated stamps a parallel animation's t0 on this same runtime as
241
452
  // `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
242
453
  // Capturing the identical value here and handing it to the integrator
@@ -247,18 +458,36 @@ export function useSmoothClipDriver(initialValue, options = {}) {
247
458
  // its rAF flush where __frameTimestamp is cleared, keeps the wall
248
459
  // clock — a lead lasting the whole animation. NaN when the globals are
249
460
  // absent (tests, non-worklets runtimes): native then falls back to its
250
- // own clock plus the min() anchor, the pre-stamp behavior.
461
+ // own clock plus the min() anchor, the pre-stamp behavior. On iOS the
462
+ // stamp is always NaN: Core Animation anchors at commit time and the
463
+ // TurboModule discards the argument, so the capture would only cost.
251
464
  const workletGlobal = globalThis;
252
- const startedAtMs = workletGlobal.__frameTimestamp || (typeof workletGlobal._getAnimationTimestamp === 'function' ? workletGlobal._getAnimationTimestamp() : Number.NaN);
465
+ const startedAtMs = !NEEDS_START_STAMP ? Number.NaN : workletGlobal.__frameTimestamp || (typeof workletGlobal._getAnimationTimestamp === 'function' ? workletGlobal._getAnimationTimestamp() : Number.NaN);
253
466
  let animationId;
254
- if (animation.type === 'timing') {
467
+ if (animation.type === 'timing' && useV2) {
468
+ const [x1, y1, x2, y2] = animation.controlPoints;
469
+ animationId = from !== undefined ? animateTimingFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, x1, y1, x2, y2, reduceMotion, startedAtMs) : animateTimingV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, x1, y1, x2, y2, reduceMotion, startedAtMs);
470
+ } else if (animation.type === 'timing') {
255
471
  const [x1, y1, x2, y2] = animation.controlPoints;
256
- animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), x1, y1, x2, y2, reduceMotion, startedAtMs);
472
+ animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0, animation.duration), x1, y1, x2, y2, reduceMotion, startedAtMs);
473
+ } else if (animation.type === 'spring' && useV2) {
474
+ const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
475
+ const mass = animation.mass ?? 1;
476
+ const stiffness = animation.stiffness ?? 100;
477
+ const damping = animation.damping ?? 10;
478
+ const initialVelocity = inheritVelocity ? 0 : animation.initialVelocity;
479
+ animationId = from !== undefined ? animateSpringFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, mass, stiffness, damping, initialVelocity, inheritVelocity, reduceMotion, startedAtMs) : animateSpringV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, mass, stiffness, damping, initialVelocity, inheritVelocity, reduceMotion, startedAtMs);
257
480
  } else if (animation.type === 'spring') {
258
481
  const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
259
- animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0.0001, animation.mass ?? 1), Math.max(0.0001, animation.stiffness ?? 100), Math.max(0, animation.damping ?? 10), inheritVelocity ? 0 : animation.initialVelocity, inheritVelocity, reduceMotion, startedAtMs);
482
+ animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0.0001, animation.mass ?? 1), Math.max(0.0001, animation.stiffness ?? 100), Math.max(0, animation.damping ?? 10), inheritVelocity ? 0 : animation.initialVelocity, inheritVelocity, reduceMotion, startedAtMs);
483
+ } else if (useV2) {
484
+ const frames = keyframeResult.frames;
485
+ animationId = from !== undefined ? animateKeyframesFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, frames, reduceMotion, startedAtMs) : animateKeyframesV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, frames, reduceMotion, startedAtMs);
260
486
  } else {
261
- animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), flattenedKeyframes(animation), reduceMotion, startedAtMs);
487
+ animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0, animation.duration),
488
+ // Non-null here: the validation gate above rejected null before any
489
+ // side effect.
490
+ flattenV1Keyframes(keyframeResult.canonicalFrames, animation.frames.map(frame => frame.offset)), reduceMotion, startedAtMs);
262
491
  }
263
492
  if (animationId === 0) {
264
493
  // The 0 sentinel: off-main, invalid-id, or otherwise unsupported
@@ -269,12 +498,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
269
498
  // effect, with a fresh id and one finished:false completion once the
270
499
  // native entry exists — in the pre-seed window rejectAnimation has no
271
500
  // entry and returns the bare 0 sentinel with no completion instead.
272
- ownership.value = INTERACTIVE;
273
- presentation.value = start;
274
- if (scalarsWereStale) scalarsStale.value = 1;
275
- activeAnimationId.value = 0;
501
+ if (!useV2) {
502
+ ownership.value = INTERACTIVE;
503
+ presentation.value = start;
504
+ if (scalarsWereStale) scalarsStale.value = 1;
505
+ activeAnimationId.value = 0;
506
+ }
276
507
  return rejectAnimationHostFunction(driverId);
277
508
  }
509
+ if (useV2) {
510
+ ownership.value = NATIVE;
511
+ presentation.value = canonicalTarget;
512
+ scalarsStale.value = 0;
513
+ }
278
514
  activeAnimationId.value = animationId;
279
515
  return animationId;
280
516
  };
@@ -284,10 +520,11 @@ export function useSmoothClipDriver(initialValue, options = {}) {
284
520
  // Past the hook's cleanup the native entry is gone; the call would fail
285
521
  // defined (handled=false) but it is the last driver.ui path that still
286
522
  // crosses into native after the tombstone. Guard it like the rest.
287
- if (disposed.value !== 0) return presentation.value;
288
- const values = cancelAnimationHostFunction(driverId, animationId, behavior === 'target' ? 1 : 0);
289
- if (values.length < 8 || values[0] !== 1) return presentation.value;
290
- const current = presentationFromNative(values, presentation.value, 1);
523
+ const fallback = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
524
+ if (disposed.value !== 0) return fallback;
525
+ const values = PRESENTATION_PROTOCOL_VERSION === 2 && cancelAnimationV2HostFunction ? cancelAnimationV2HostFunction(driverId, animationId, behavior === 'target' ? 1 : 0) : cancelAnimationHostFunction(driverId, animationId, behavior === 'target' ? 1 : 0);
526
+ if (values.length < 8 || values[0] !== 1) return fallback;
527
+ const current = presentationFromNative(values, fallback, 1);
291
528
  activeAnimationId.value = 0;
292
529
  ownership.value = INTERACTIVE;
293
530
  scalarsStale.value = 0;
@@ -316,6 +553,12 @@ export function useSmoothClipDriver(initialValue, options = {}) {
316
553
  if (isRNRuntime()) return uiOnly();
317
554
  setScalarsOnUI(x, y, width, height, radius, contentTranslateX, contentTranslateY);
318
555
  },
556
+ setPresentationScalars(x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale) {
557
+ 'worklet';
558
+
559
+ if (isRNRuntime()) return uiOnly();
560
+ setPresentationScalarsOnUI(x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale);
561
+ },
319
562
  animateTo(target, animation) {
320
563
  'worklet';
321
564
 
@@ -391,6 +634,13 @@ export function useSmoothClipDriver(initialValue, options = {}) {
391
634
  });
392
635
  return promise;
393
636
  }
637
+ },
638
+ __smoothClipHandle: {
639
+ driverId,
640
+ presentation,
641
+ ownership,
642
+ activeAnimationId,
643
+ disposed
394
644
  }
395
645
  };
396
646
  setDriverState(driver, createDriverState(driverId, initialPresentation, presentation, callbackRef, activeAnimationId, ownership));
@@ -413,7 +663,7 @@ export function useSmoothClipDriver(initialValue, options = {}) {
413
663
  synchronizeNativeCompletion(activeAnimationId, ownership, result.animationId, result.finished);
414
664
  completeNativeAnimation(driverId, result.animationId, result.finished);
415
665
  });
416
- scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, gone, setter) => {
666
+ scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, gone, setter, setterV2, protocolVersion) => {
417
667
  'worklet';
418
668
 
419
669
  // First, before the listener or the seed: this effect run owns the
@@ -422,7 +672,8 @@ export function useSmoothClipDriver(initialValue, options = {}) {
422
672
  gone.value = 0;
423
673
  let last = null;
424
674
  const deliver = next => {
425
- if (!isFiniteClipPresentation(next) || clipPresentationEquals(last, next)) {
675
+ const canonical = canonicalizeClipPresentation(next);
676
+ if (canonical === null || clipPresentationEquals(last, canonical)) {
426
677
  return;
427
678
  }
428
679
  // Record `last` only for values native has actually observed:
@@ -431,17 +682,27 @@ export function useSmoothClipDriver(initialValue, options = {}) {
431
682
  if (owner.value !== INTERACTIVE) return;
432
683
  if (suppressed.value > 0) {
433
684
  suppressed.value -= 1;
434
- last = next;
685
+ last = canonical;
435
686
  return;
436
687
  }
437
- last = next;
688
+ if (protocolVersion !== 2 && !isV1Compatible(canonical)) {
689
+ return;
690
+ }
691
+ last = canonical;
438
692
  if (stale.value !== 0) stale.value = 0;
439
693
  const {
440
694
  clip,
441
695
  contentTranslateX,
442
- contentTranslateY
443
- } = next;
444
- setter(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, false, false);
696
+ contentTranslateY,
697
+ contentScale
698
+ } = canonical;
699
+ // Declarative deliveries always record velocity samples — only the
700
+ // setScalars hot path is gated by the velocityTracking option.
701
+ if (protocolVersion === 2 && setterV2) {
702
+ setterV2(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(canonical), contentTranslateX, contentTranslateY, contentScale, false, false, true);
703
+ } else {
704
+ setter(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, false, false);
705
+ }
445
706
  };
446
707
  source.addListener(listenerId, deliver);
447
708
  // Authoritative take-ownership seed. Creates the native entry before
@@ -450,15 +711,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
450
711
  // An animation worklet can win the scheduling race and create a latch
451
712
  // first; Native ownership then marks that intent as newer, so this
452
713
  // passive seed must not clear its active id or replay the target value.
453
- const current = source.value;
454
- if (owner.value === INTERACTIVE && isFiniteClipPresentation(current)) {
714
+ const current = canonicalizeClipPresentation(source.value);
715
+ if (owner.value === INTERACTIVE && current !== null) {
455
716
  active.value = 0;
456
717
  owner.value = INTERACTIVE;
457
718
  stale.value = 0;
458
719
  last = current;
459
- setter(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.radius, current.contentTranslateX, current.contentTranslateY, true, false);
720
+ if (protocolVersion === 2 && setterV2) {
721
+ setterV2(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.topLeftRadius, current.clip.topRightRadius, current.clip.bottomRightRadius, current.clip.bottomLeftRadius, curveCode(current), current.contentTranslateX, current.contentTranslateY, current.contentScale, true, false, true);
722
+ } else if (isV1Compatible(current)) {
723
+ setter(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.radius, current.contentTranslateX, current.contentTranslateY, true, false);
724
+ }
460
725
  }
461
- }, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, disposed, setPresentationHostFunction);
726
+ }, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, disposed, setPresentationHostFunction, setPresentationV2HostFunction, PRESENTATION_PROTOCOL_VERSION);
462
727
  return () => {
463
728
  subscription.remove();
464
729
  rejectDriverRequests(driverId);