react-native-smooth-clip-view 0.2.9 → 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 (130) hide show
  1. package/README.md +60 -21
  2. package/android/build.gradle +6 -2
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +890 -4
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +1056 -44
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +371 -18
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  12. package/cpp/SmoothClipAnimationCurve.h +151 -122
  13. package/cpp/SmoothClipRegistry.h +95 -3
  14. package/cpp/SmoothClipSharedGeometry.h +146 -7
  15. package/cpp/SmoothClipVelocityTracker.h +13 -9
  16. package/ios/SmoothClipGeometry.h +184 -6
  17. package/ios/SmoothClipRegistry.mm +1028 -22
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +857 -2
  20. package/ios/SmoothClipTurboModule.h +310 -0
  21. package/ios/SmoothClipView.mm +714 -183
  22. package/ios/SmoothClipViewRegistry.h +1 -0
  23. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  24. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  26. package/lib/module/SmoothClipView.ios.js +22 -6
  27. package/lib/module/SmoothClipView.ios.js.map +1 -1
  28. package/lib/module/SmoothClipView.js +1 -46
  29. package/lib/module/SmoothClipView.js.map +1 -1
  30. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +36 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +11 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/driverState.js +0 -28
  38. package/lib/module/driverState.js.map +1 -1
  39. package/lib/module/drivers.js +1 -281
  40. package/lib/module/drivers.js.map +1 -1
  41. package/lib/module/drivers.native.js +263 -63
  42. package/lib/module/drivers.native.js.map +1 -1
  43. package/lib/module/geometry.js +147 -17
  44. package/lib/module/geometry.js.map +1 -1
  45. package/lib/module/groupDriverTypes.js +4 -0
  46. package/lib/module/groupDriverTypes.js.map +1 -0
  47. package/lib/module/groupDrivers.android.js +4 -0
  48. package/lib/module/groupDrivers.android.js.map +1 -0
  49. package/lib/module/groupDrivers.ios.js +4 -0
  50. package/lib/module/groupDrivers.ios.js.map +1 -0
  51. package/lib/module/groupDrivers.js +4 -0
  52. package/lib/module/groupDrivers.js.map +1 -0
  53. package/lib/module/groupDrivers.native.js +799 -0
  54. package/lib/module/groupDrivers.native.js.map +1 -0
  55. package/lib/module/index.js +3 -2
  56. package/lib/module/index.js.map +1 -1
  57. package/lib/module/presentationProtocol.js +15 -0
  58. package/lib/module/presentationProtocol.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  61. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  62. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  64. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  66. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  67. package/lib/typescript/src/capabilities.d.ts +3 -0
  68. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  69. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  72. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  73. package/lib/typescript/src/driverState.d.ts +1 -6
  74. package/lib/typescript/src/driverState.d.ts.map +1 -1
  75. package/lib/typescript/src/driverTypes.d.ts +25 -11
  76. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  77. package/lib/typescript/src/drivers.d.ts +2 -4
  78. package/lib/typescript/src/drivers.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  80. package/lib/typescript/src/geometry.d.ts +36 -2
  81. package/lib/typescript/src/geometry.d.ts.map +1 -1
  82. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  83. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  85. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  91. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/index.d.ts +6 -3
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  95. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  96. package/package.json +10 -11
  97. package/src/NativeSmoothClipModule.ts +275 -0
  98. package/src/SmoothClipView.ios.tsx +30 -11
  99. package/src/SmoothClipView.tsx +2 -76
  100. package/src/SmoothClipViewNativeComponent.ts +27 -1
  101. package/src/capabilities.native.ts +69 -0
  102. package/src/capabilities.ts +2 -0
  103. package/src/capabilityTypes.ts +17 -0
  104. package/src/driverState.ts +1 -41
  105. package/src/driverTypes.ts +42 -11
  106. package/src/drivers.native.ts +824 -124
  107. package/src/drivers.ts +2 -423
  108. package/src/geometry.ts +231 -27
  109. package/src/groupDriverTypes.ts +117 -0
  110. package/src/groupDrivers.android.ts +1 -0
  111. package/src/groupDrivers.ios.ts +1 -0
  112. package/src/groupDrivers.native.ts +1189 -0
  113. package/src/groupDrivers.ts +1 -0
  114. package/src/index.ts +30 -2
  115. package/src/presentationProtocol.ts +28 -0
  116. package/lib/module/SmoothClipView.web.js +0 -54
  117. package/lib/module/SmoothClipView.web.js.map +0 -1
  118. package/lib/module/index.web.js +0 -8
  119. package/lib/module/index.web.js.map +0 -1
  120. package/lib/module/webClipPath.js +0 -25
  121. package/lib/module/webClipPath.js.map +0 -1
  122. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  123. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  124. package/lib/typescript/src/index.web.d.ts +0 -9
  125. package/lib/typescript/src/index.web.d.ts.map +0 -1
  126. package/lib/typescript/src/webClipPath.d.ts +0 -7
  127. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  128. package/src/SmoothClipView.web.tsx +0 -66
  129. package/src/index.web.ts +0 -23
  130. package/src/webClipPath.ts +0 -30
@@ -0,0 +1,1189 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { Platform } from 'react-native';
3
+ import { useSharedValue } from 'react-native-reanimated';
4
+ import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
5
+ import NativeSmoothClipModule from './smoothClipNative';
6
+ import { getSmoothClipCapabilities } from './capabilities';
7
+ import { allocateDriverId } from './driverState';
8
+ import type { SmoothClipDriver, SmoothClipDriverHandle } from './driverTypes';
9
+ import type {
10
+ SmoothClipBatchEntry,
11
+ SmoothClipGroupAnimationResult,
12
+ SmoothClipGroupCancelBehavior,
13
+ SmoothClipGroupDriver,
14
+ SmoothClipGroupDriverOptions,
15
+ SmoothClipGroupKeyframeAnimation,
16
+ SmoothClipGroupKeyframeEntry,
17
+ SmoothClipGroupMotionAnimation,
18
+ SmoothClipGroupMotionEntry,
19
+ SmoothClipGroupSnapshot,
20
+ } from './groupDriverTypes';
21
+ import {
22
+ canonicalizeClipPresentation,
23
+ clipPresentationEquals,
24
+ type CanonicalSmoothClipPresentation,
25
+ } from './geometry';
26
+
27
+ const INTERACTIVE = 0;
28
+ const NATIVE = 1;
29
+ const PRESENTATION_STRIDE = 12;
30
+ const SNAPSHOT_STRIDE = 13;
31
+ const MOTION_ENTRY_STRIDE = 26;
32
+ const NEEDS_START_STAMP = Platform.OS === 'android';
33
+ const GROUPS_SUPPORTED = getSmoothClipCapabilities().groups;
34
+
35
+ type NativeGroupHostFunctions = Readonly<{
36
+ beginGroupInteractionV2(driverIds: readonly number[]): readonly number[];
37
+ snapshotGroupV2(driverIds: readonly number[]): readonly number[];
38
+ setClipPresentationBatchV2(entries: readonly number[]): boolean;
39
+ animateTimingGroupV2(...args: readonly unknown[]): number;
40
+ animateSpringGroupV2(...args: readonly unknown[]): number;
41
+ animateKeyframesGroupV2(...args: readonly unknown[]): number;
42
+ cancelAnimationGroupV2(groupId: number, behavior: number): readonly number[];
43
+ onClipGroupAnimationComplete?: typeof NativeSmoothClipModule.onClipGroupAnimationComplete;
44
+ }>;
45
+ type RequiredNativeGroupCommands = Required<
46
+ Omit<NativeGroupHostFunctions, 'onClipGroupAnimationComplete'>
47
+ >;
48
+
49
+ const nativeGroups =
50
+ NativeSmoothClipModule as unknown as Partial<NativeGroupHostFunctions>;
51
+ const beginGroupInteractionV2HostFunction =
52
+ nativeGroups.beginGroupInteractionV2;
53
+ const snapshotGroupV2HostFunction = nativeGroups.snapshotGroupV2;
54
+ const setClipPresentationBatchV2HostFunction =
55
+ nativeGroups.setClipPresentationBatchV2;
56
+ const animateTimingGroupV2HostFunction = nativeGroups.animateTimingGroupV2;
57
+ const animateSpringGroupV2HostFunction = nativeGroups.animateSpringGroupV2;
58
+ const animateKeyframesGroupV2HostFunction =
59
+ nativeGroups.animateKeyframesGroupV2;
60
+ const cancelAnimationGroupV2HostFunction = nativeGroups.cancelAnimationGroupV2;
61
+
62
+ type RuntimeGroupRecord = Readonly<{
63
+ controllerId: number;
64
+ drivers: readonly SmoothClipDriver[];
65
+ }>;
66
+
67
+ type NativeGroupDriverState = {
68
+ callbackRef: {
69
+ current: SmoothClipGroupDriverOptions['onAnimationComplete'];
70
+ };
71
+ completionDeferrals: number;
72
+ deferredCompletions: SmoothClipGroupAnimationResult[];
73
+ effectGeneration: number;
74
+ };
75
+
76
+ const nativeGroupDriverStates = new Map<number, NativeGroupDriverState>();
77
+
78
+ function deliverNativeGroupCompletion(
79
+ controllerId: number,
80
+ groupId: number,
81
+ finished: boolean
82
+ ): void {
83
+ const state = nativeGroupDriverStates.get(controllerId);
84
+ if (!state) return;
85
+ const result = { groupId, finished };
86
+ if (state.completionDeferrals > 0) {
87
+ state.deferredCompletions.push(result);
88
+ return;
89
+ }
90
+ state.callbackRef.current?.(result);
91
+ }
92
+
93
+ function deferNativeGroupCompletions(controllerId: number): void {
94
+ const state = nativeGroupDriverStates.get(controllerId);
95
+ if (state) state.completionDeferrals += 1;
96
+ }
97
+
98
+ function releaseNativeGroupCompletions(controllerId: number): void {
99
+ const state = nativeGroupDriverStates.get(controllerId);
100
+ if (!state || state.completionDeferrals === 0) return;
101
+ state.completionDeferrals -= 1;
102
+ if (state.completionDeferrals !== 0) return;
103
+ const deferred = state.deferredCompletions.splice(0);
104
+ for (const result of deferred) state.callbackRef.current?.(result);
105
+ }
106
+
107
+ type GroupRuntimeGlobal = typeof globalThis & {
108
+ __smoothClipGroupRecords?: Record<string, RuntimeGroupRecord>;
109
+ };
110
+
111
+ function runtimeGroupRecords(): Record<string, RuntimeGroupRecord> {
112
+ 'worklet';
113
+ const runtime = globalThis as GroupRuntimeGlobal;
114
+ runtime.__smoothClipGroupRecords ??= {};
115
+ return runtime.__smoothClipGroupRecords;
116
+ }
117
+
118
+ function fail(message: string): never {
119
+ 'worklet';
120
+ throw new Error(`[SmoothClipView] ${message}`);
121
+ }
122
+
123
+ function uiOnly(): never {
124
+ 'worklet';
125
+ return fail(
126
+ 'group.ui methods must run on the UI runtime. Use group.react from React code.'
127
+ );
128
+ }
129
+
130
+ function curveCode(presentation: CanonicalSmoothClipPresentation): number {
131
+ 'worklet';
132
+ return presentation.clip.curve === 'continuous' ? 1 : 0;
133
+ }
134
+
135
+ function appendPresentation(
136
+ values: number[],
137
+ presentation: CanonicalSmoothClipPresentation
138
+ ): void {
139
+ 'worklet';
140
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
141
+ presentation;
142
+ values.push(
143
+ clip.x,
144
+ clip.y,
145
+ clip.width,
146
+ clip.height,
147
+ clip.topLeftRadius,
148
+ clip.topRightRadius,
149
+ clip.bottomRightRadius,
150
+ clip.bottomLeftRadius,
151
+ curveCode(presentation),
152
+ contentTranslateX,
153
+ contentTranslateY,
154
+ contentScale
155
+ );
156
+ }
157
+
158
+ function presentationFromValues(
159
+ values: readonly number[],
160
+ offset: number
161
+ ): CanonicalSmoothClipPresentation | null {
162
+ 'worklet';
163
+ if (values.length < offset + PRESENTATION_STRIDE) return null;
164
+ return canonicalizeClipPresentation({
165
+ clip: {
166
+ x: values[offset] as number,
167
+ y: values[offset + 1] as number,
168
+ width: values[offset + 2] as number,
169
+ height: values[offset + 3] as number,
170
+ radius: 0,
171
+ topLeftRadius: values[offset + 4] as number,
172
+ topRightRadius: values[offset + 5] as number,
173
+ bottomRightRadius: values[offset + 6] as number,
174
+ bottomLeftRadius: values[offset + 7] as number,
175
+ curve: values[offset + 8] === 1 ? 'continuous' : 'circular',
176
+ },
177
+ contentTranslateX: values[offset + 9] as number,
178
+ contentTranslateY: values[offset + 10] as number,
179
+ contentScale: values[offset + 11] as number,
180
+ });
181
+ }
182
+
183
+ type ValidDriver = Readonly<{
184
+ driver: SmoothClipDriver;
185
+ handle: SmoothClipDriverHandle;
186
+ }>;
187
+
188
+ function validateDrivers(
189
+ drivers: readonly SmoothClipDriver[]
190
+ ): readonly ValidDriver[] {
191
+ 'worklet';
192
+ if (drivers.length === 0) return fail('A group must contain a driver.');
193
+ const seen: Record<string, true> = {};
194
+ const result: ValidDriver[] = [];
195
+ for (const driver of drivers) {
196
+ const handle = driver.__smoothClipHandle;
197
+ if (
198
+ driver.kind !== 'hybrid' ||
199
+ handle === undefined ||
200
+ !Number.isSafeInteger(handle.driverId) ||
201
+ handle.driverId <= 0 ||
202
+ handle.disposed.value !== 0
203
+ ) {
204
+ return fail('Every group participant must be a live SmoothClip driver.');
205
+ }
206
+ const key = String(handle.driverId);
207
+ if (seen[key]) return fail('A group cannot contain duplicate drivers.');
208
+ seen[key] = true;
209
+ result.push({ driver, handle });
210
+ }
211
+ return result;
212
+ }
213
+
214
+ function requireNativeGroups(): RequiredNativeGroupCommands {
215
+ 'worklet';
216
+ if (
217
+ !GROUPS_SUPPORTED ||
218
+ beginGroupInteractionV2HostFunction === undefined ||
219
+ snapshotGroupV2HostFunction === undefined ||
220
+ setClipPresentationBatchV2HostFunction === undefined ||
221
+ animateTimingGroupV2HostFunction === undefined ||
222
+ animateSpringGroupV2HostFunction === undefined ||
223
+ animateKeyframesGroupV2HostFunction === undefined ||
224
+ cancelAnimationGroupV2HostFunction === undefined
225
+ ) {
226
+ return fail('Native grouped presentation protocol V2 is unavailable.');
227
+ }
228
+ return {
229
+ beginGroupInteractionV2: beginGroupInteractionV2HostFunction,
230
+ snapshotGroupV2: snapshotGroupV2HostFunction,
231
+ setClipPresentationBatchV2: setClipPresentationBatchV2HostFunction,
232
+ animateTimingGroupV2: animateTimingGroupV2HostFunction,
233
+ animateSpringGroupV2: animateSpringGroupV2HostFunction,
234
+ animateKeyframesGroupV2: animateKeyframesGroupV2HostFunction,
235
+ cancelAnimationGroupV2: cancelAnimationGroupV2HostFunction,
236
+ };
237
+ }
238
+
239
+ type ParsedGroupSnapshots = Readonly<{
240
+ snapshots: readonly SmoothClipGroupSnapshot[];
241
+ unavailable: boolean;
242
+ }>;
243
+
244
+ function parseSnapshots(
245
+ drivers: readonly SmoothClipDriver[],
246
+ values: readonly number[]
247
+ ): ParsedGroupSnapshots {
248
+ 'worklet';
249
+ if (values.length !== drivers.length * SNAPSHOT_STRIDE) {
250
+ return fail('Native returned an invalid group snapshot.');
251
+ }
252
+ const snapshots: SmoothClipGroupSnapshot[] = [];
253
+ let unavailable = false;
254
+ for (let index = 0; index < drivers.length; index += 1) {
255
+ const offset = index * SNAPSHOT_STRIDE;
256
+ const readyCode = values[offset];
257
+ let presentation = presentationFromValues(values, offset + 1);
258
+ if (readyCode !== 0 && readyCode !== 1) {
259
+ return fail('Native returned an invalid group snapshot.');
260
+ }
261
+ if (presentation === null && readyCode === 0) {
262
+ presentation = canonicalizeClipPresentation(
263
+ drivers[index]?.__smoothClipHandle?.presentation.value as never
264
+ );
265
+ unavailable = true;
266
+ }
267
+ if (presentation === null) {
268
+ return fail('Native returned an invalid group snapshot.');
269
+ }
270
+ snapshots.push({
271
+ driver: drivers[index] as SmoothClipDriver,
272
+ presentation,
273
+ ready: readyCode === 1,
274
+ });
275
+ }
276
+ return { snapshots, unavailable };
277
+ }
278
+
279
+ function applyInteractiveSnapshots(
280
+ snapshots: readonly SmoothClipGroupSnapshot[]
281
+ ): void {
282
+ 'worklet';
283
+ // Keep individual driver listeners from re-emitting a transaction that the
284
+ // group engine has already committed atomically.
285
+ for (const snapshot of snapshots) {
286
+ const handle = snapshot.driver.__smoothClipHandle;
287
+ if (handle) handle.ownership.value = NATIVE;
288
+ }
289
+ for (const snapshot of snapshots) {
290
+ const handle = snapshot.driver.__smoothClipHandle;
291
+ if (!handle) continue;
292
+ handle.activeAnimationId.value = 0;
293
+ handle.presentation.value = snapshot.presentation;
294
+ handle.ownership.value = INTERACTIVE;
295
+ }
296
+ }
297
+
298
+ function suspensionPolicyCode(value: 'pause' | 'finish' | undefined): number {
299
+ 'worklet';
300
+ return value === 'finish' ? 1 : 0;
301
+ }
302
+
303
+ function reduceMotionCode(value: 'system' | 'always' | 'never'): number {
304
+ 'worklet';
305
+ if (value === 'always') return 1;
306
+ if (value === 'never') return 2;
307
+ return 0;
308
+ }
309
+
310
+ function animationStartTimestamp(): number {
311
+ 'worklet';
312
+ if (!NEEDS_START_STAMP) return Number.NaN;
313
+ const runtime = globalThis as {
314
+ __frameTimestamp?: number;
315
+ _getAnimationTimestamp?: () => number;
316
+ };
317
+ return (
318
+ runtime.__frameTimestamp ||
319
+ (typeof runtime._getAnimationTimestamp === 'function'
320
+ ? runtime._getAnimationTimestamp()
321
+ : Number.NaN)
322
+ );
323
+ }
324
+
325
+ function animationIsFinite(
326
+ animation: SmoothClipGroupMotionAnimation | SmoothClipGroupKeyframeAnimation
327
+ ): boolean {
328
+ 'worklet';
329
+ if (
330
+ animation.suspensionPolicy !== undefined &&
331
+ animation.suspensionPolicy !== 'pause' &&
332
+ animation.suspensionPolicy !== 'finish'
333
+ ) {
334
+ return false;
335
+ }
336
+ if (animation.type === 'timing') {
337
+ if (
338
+ !Array.isArray(animation.controlPoints) ||
339
+ animation.controlPoints.length !== 4
340
+ ) {
341
+ return false;
342
+ }
343
+ const [x1, y1, x2, y2] = animation.controlPoints;
344
+ return (
345
+ Number.isFinite(animation.duration) &&
346
+ animation.duration >= 0 &&
347
+ [x1, y1, x2, y2].every(Number.isFinite) &&
348
+ x1 >= 0 &&
349
+ x1 <= 1 &&
350
+ x2 >= 0 &&
351
+ x2 <= 1
352
+ );
353
+ }
354
+ if (animation.type === 'spring') {
355
+ const initialVelocity =
356
+ animation.initialVelocity === 'inherit' ||
357
+ animation.initialVelocity === undefined
358
+ ? 0
359
+ : animation.initialVelocity;
360
+ return (
361
+ [
362
+ animation.mass ?? 1,
363
+ animation.stiffness ?? 100,
364
+ animation.damping ?? 10,
365
+ initialVelocity,
366
+ ].every(Number.isFinite) &&
367
+ (animation.mass ?? 1) > 0 &&
368
+ (animation.stiffness ?? 100) > 0 &&
369
+ (animation.damping ?? 10) >= 0
370
+ );
371
+ }
372
+ return (
373
+ animation.type === 'keyframes' &&
374
+ Number.isFinite(animation.duration) &&
375
+ animation.duration >= 0
376
+ );
377
+ }
378
+
379
+ function springScaleIsProvablyPositive(
380
+ starts: readonly CanonicalSmoothClipPresentation[],
381
+ targets: readonly CanonicalSmoothClipPresentation[],
382
+ animation: Extract<SmoothClipGroupMotionAnimation, { type: 'spring' }>
383
+ ): boolean {
384
+ 'worklet';
385
+ let changesScale = false;
386
+ for (let index = 0; index < starts.length; index += 1) {
387
+ if (starts[index]?.contentScale !== targets[index]?.contentScale) {
388
+ changesScale = true;
389
+ break;
390
+ }
391
+ }
392
+ if (!changesScale) return true;
393
+
394
+ // A zero-velocity critically/over-damped unit-step response is monotonic,
395
+ // so interpolation stays between two already-positive endpoints. Other
396
+ // configurations may overshoot; callers can compile those to keyframes.
397
+ const mass = animation.mass ?? 1;
398
+ const stiffness = animation.stiffness ?? 100;
399
+ const damping = animation.damping ?? 10;
400
+ return (
401
+ animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness
402
+ );
403
+ }
404
+
405
+ function snapshotsForMotionStarts(
406
+ validDrivers: readonly ValidDriver[],
407
+ entries: readonly SmoothClipGroupMotionEntry[]
408
+ ): readonly CanonicalSmoothClipPresentation[] {
409
+ 'worklet';
410
+ const missingFrom = entries.some((entry) => entry.from === undefined);
411
+ const sampled = missingFrom
412
+ ? parseSnapshots(
413
+ validDrivers.map((entry) => entry.driver),
414
+ requireNativeGroups().snapshotGroupV2(
415
+ validDrivers.map((entry) => entry.handle.driverId)
416
+ )
417
+ ).snapshots
418
+ : [];
419
+ const starts: CanonicalSmoothClipPresentation[] = [];
420
+ for (let index = 0; index < entries.length; index += 1) {
421
+ const explicit = entries[index]?.from;
422
+ const presentation =
423
+ explicit === undefined
424
+ ? (sampled[index]?.presentation ?? null)
425
+ : canonicalizeClipPresentation(explicit);
426
+ if (presentation === null) return fail('A group start is invalid.');
427
+ starts.push(presentation);
428
+ }
429
+ return starts;
430
+ }
431
+
432
+ function registerNativeGroup(
433
+ controllerId: number,
434
+ groupId: number,
435
+ drivers: readonly SmoothClipDriver[],
436
+ targets: readonly CanonicalSmoothClipPresentation[]
437
+ ): void {
438
+ 'worklet';
439
+ const records = runtimeGroupRecords();
440
+ records[String(groupId)] = { controllerId, drivers };
441
+ for (let index = 0; index < drivers.length; index += 1) {
442
+ const handle = drivers[index]?.__smoothClipHandle;
443
+ const target = targets[index];
444
+ if (!handle || !target) continue;
445
+ handle.ownership.value = NATIVE;
446
+ handle.activeAnimationId.value = groupId;
447
+ handle.presentation.value = target;
448
+ }
449
+ }
450
+
451
+ function synchronizeGroupCompletionOnUI(
452
+ controllerId: number,
453
+ groupId: number,
454
+ finished: boolean,
455
+ driverIds: readonly number[]
456
+ ): void {
457
+ 'worklet';
458
+ const records = runtimeGroupRecords();
459
+ const record = records[String(groupId)];
460
+ if (
461
+ !record ||
462
+ record.controllerId !== controllerId ||
463
+ record.drivers.length !== driverIds.length
464
+ ) {
465
+ return;
466
+ }
467
+ for (let index = 0; index < record.drivers.length; index += 1) {
468
+ if (
469
+ record.drivers[index]?.__smoothClipHandle?.driverId !== driverIds[index]
470
+ ) {
471
+ return;
472
+ }
473
+ }
474
+ let snapshots: readonly SmoothClipGroupSnapshot[] = [];
475
+ if (!finished) {
476
+ try {
477
+ snapshots = parseSnapshots(
478
+ record.drivers,
479
+ requireNativeGroups().snapshotGroupV2(
480
+ record.drivers.map(
481
+ (driver) => driver.__smoothClipHandle?.driverId ?? 0
482
+ )
483
+ )
484
+ ).snapshots;
485
+ } catch {
486
+ snapshots = [];
487
+ }
488
+ }
489
+ for (let index = 0; index < record.drivers.length; index += 1) {
490
+ const driver = record.drivers[index];
491
+ if (driver === undefined) continue;
492
+ const handle = driver.__smoothClipHandle;
493
+ if (handle === undefined || handle.activeAnimationId.value !== groupId) {
494
+ continue;
495
+ }
496
+ const snapshot = snapshots[index];
497
+ if (snapshot) handle.presentation.value = snapshot.presentation;
498
+ handle.activeAnimationId.value = 0;
499
+ handle.ownership.value = INTERACTIVE;
500
+ }
501
+ delete records[String(groupId)];
502
+ }
503
+
504
+ type PendingGroupRequest = Readonly<{
505
+ resolve(value: unknown): void;
506
+ reject(error: Error): void;
507
+ teardownValue?: unknown;
508
+ releasesCompletions?: boolean;
509
+ }>;
510
+
511
+ const pendingGroupRequests = new Map<number, PendingGroupRequest>();
512
+ const requestIdsByController = new Map<number, Set<number>>();
513
+ let nextGroupRequestId = 0;
514
+
515
+ function createGroupRequest<T>(
516
+ controllerId: number,
517
+ teardownResolution?: Readonly<{ value: unknown }>,
518
+ deferCompletions = false
519
+ ): Readonly<{ requestId: number; promise: Promise<T> }> {
520
+ nextGroupRequestId = (nextGroupRequestId % 0x7ffffffe) + 1;
521
+ const requestId = nextGroupRequestId;
522
+ let resolveRequest!: (value: T) => void;
523
+ let rejectRequest!: (error: Error) => void;
524
+ const promise = new Promise<T>((resolve, reject) => {
525
+ resolveRequest = resolve;
526
+ rejectRequest = reject;
527
+ });
528
+ pendingGroupRequests.set(requestId, {
529
+ resolve: resolveRequest as (value: unknown) => void,
530
+ reject: rejectRequest,
531
+ ...(teardownResolution === undefined
532
+ ? {}
533
+ : { teardownValue: teardownResolution.value }),
534
+ ...(deferCompletions ? { releasesCompletions: true } : {}),
535
+ });
536
+ const ids = requestIdsByController.get(controllerId) ?? new Set<number>();
537
+ ids.add(requestId);
538
+ requestIdsByController.set(controllerId, ids);
539
+ if (deferCompletions) deferNativeGroupCompletions(controllerId);
540
+ return { requestId, promise };
541
+ }
542
+
543
+ function settleGroupRequest(
544
+ controllerId: number,
545
+ requestId: number,
546
+ succeeded: boolean,
547
+ value: unknown,
548
+ message: string
549
+ ): void {
550
+ const request = pendingGroupRequests.get(requestId);
551
+ pendingGroupRequests.delete(requestId);
552
+ const ids = requestIdsByController.get(controllerId);
553
+ ids?.delete(requestId);
554
+ if (ids?.size === 0) requestIdsByController.delete(controllerId);
555
+ if (!request) return;
556
+ if (succeeded) request.resolve(value);
557
+ else request.reject(new Error(message));
558
+ if (request.releasesCompletions) {
559
+ // One turn resolves the Promise; the second lets reactions already
560
+ // attached to that Promise observe settlement before callbacks run.
561
+ queueMicrotask(() => {
562
+ queueMicrotask(() => releaseNativeGroupCompletions(controllerId));
563
+ });
564
+ }
565
+ }
566
+
567
+ function rejectGroupRequests(controllerId: number): void {
568
+ const ids = requestIdsByController.get(controllerId);
569
+ if (ids) {
570
+ for (const requestId of ids) {
571
+ const request = pendingGroupRequests.get(requestId);
572
+ pendingGroupRequests.delete(requestId);
573
+ if (!request) continue;
574
+ if ('teardownValue' in request) request.resolve(request.teardownValue);
575
+ else
576
+ request.reject(
577
+ new Error('[SmoothClipView] Group driver was destroyed.')
578
+ );
579
+ }
580
+ requestIdsByController.delete(controllerId);
581
+ }
582
+ const state = nativeGroupDriverStates.get(controllerId);
583
+ if (state) state.completionDeferrals = 0;
584
+ }
585
+
586
+ function detachNativeGroupDriverState(
587
+ controllerId: number,
588
+ state: NativeGroupDriverState
589
+ ): void {
590
+ state.completionDeferrals = 0;
591
+ const deferred = state.deferredCompletions.splice(0);
592
+ if (nativeGroupDriverStates.get(controllerId) === state) {
593
+ nativeGroupDriverStates.delete(controllerId);
594
+ }
595
+ if (deferred.length === 0) return;
596
+ queueMicrotask(() => {
597
+ queueMicrotask(() => {
598
+ for (const result of deferred) state.callbackRef.current?.(result);
599
+ });
600
+ });
601
+ }
602
+
603
+ function queueNativeTeardownCompletion(
604
+ controllerId: number,
605
+ groupId: number
606
+ ): void {
607
+ const state = nativeGroupDriverStates.get(controllerId);
608
+ if (!state) return;
609
+ queueMicrotask(() => {
610
+ queueMicrotask(() => {
611
+ state.callbackRef.current?.({ groupId, finished: false });
612
+ });
613
+ });
614
+ }
615
+
616
+ function finalizeNativeGroupDriverTeardown(
617
+ controllerId: number,
618
+ effectGeneration: number
619
+ ): void {
620
+ const state = nativeGroupDriverStates.get(controllerId);
621
+ if (!state || state.effectGeneration !== effectGeneration) return;
622
+ detachNativeGroupDriverState(controllerId, state);
623
+ }
624
+
625
+ function errorMessage(error: unknown): string {
626
+ 'worklet';
627
+ return error instanceof Error
628
+ ? error.message
629
+ : '[SmoothClipView] Group operation failed.';
630
+ }
631
+
632
+ export function useSmoothClipGroupDriver(
633
+ options: SmoothClipGroupDriverOptions = {}
634
+ ): SmoothClipGroupDriver {
635
+ const callbackRef = useRef(options.onAnimationComplete);
636
+ const stateRef = useRef<NativeGroupDriverState | null>(null);
637
+ const groupRef = useRef<SmoothClipGroupDriver | null>(null);
638
+ const disposed = useSharedValue(0);
639
+ callbackRef.current = options.onAnimationComplete;
640
+ if (stateRef.current === null) {
641
+ stateRef.current = {
642
+ callbackRef,
643
+ completionDeferrals: 0,
644
+ deferredCompletions: [],
645
+ effectGeneration: 0,
646
+ };
647
+ }
648
+
649
+ if (groupRef.current === null) {
650
+ const controllerId = allocateDriverId();
651
+ const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
652
+
653
+ const beginOnUI = (
654
+ drivers: readonly SmoothClipDriver[]
655
+ ): readonly SmoothClipGroupSnapshot[] => {
656
+ 'worklet';
657
+ if (disposed.value !== 0) return fail('Group driver was destroyed.');
658
+ const valid = validateDrivers(drivers);
659
+ const values = requireNativeGroups().beginGroupInteractionV2(
660
+ valid.map((entry) => entry.handle.driverId)
661
+ );
662
+ const parsed = parseSnapshots(drivers, values);
663
+ // An unavailable wire presentation means native rejected the atomic
664
+ // interaction preflight. Return readiness without changing JS ownership.
665
+ if (!parsed.unavailable) applyInteractiveSnapshots(parsed.snapshots);
666
+ return parsed.snapshots;
667
+ };
668
+
669
+ const snapshotOnUI = (
670
+ drivers: readonly SmoothClipDriver[]
671
+ ): readonly SmoothClipGroupSnapshot[] => {
672
+ 'worklet';
673
+ if (disposed.value !== 0) return fail('Group driver was destroyed.');
674
+ const valid = validateDrivers(drivers);
675
+ return parseSnapshots(
676
+ drivers,
677
+ requireNativeGroups().snapshotGroupV2(
678
+ valid.map((entry) => entry.handle.driverId)
679
+ )
680
+ ).snapshots;
681
+ };
682
+
683
+ const setBatchOnUI = (entries: readonly SmoothClipBatchEntry[]): void => {
684
+ 'worklet';
685
+ if (disposed.value !== 0) return fail('Group driver was destroyed.');
686
+ const valid = validateDrivers(entries.map((entry) => entry.driver));
687
+ const canonical: CanonicalSmoothClipPresentation[] = [];
688
+ const values: number[] = [];
689
+ for (let index = 0; index < entries.length; index += 1) {
690
+ const presentation = canonicalizeClipPresentation(
691
+ entries[index]?.presentation as never
692
+ );
693
+ if (presentation === null)
694
+ return fail('A batch presentation is invalid.');
695
+ canonical.push(presentation);
696
+ values.push(valid[index]?.handle.driverId as number);
697
+ appendPresentation(values, presentation);
698
+ }
699
+ if (!requireNativeGroups().setClipPresentationBatchV2(values)) {
700
+ return fail('Native rejected the complete presentation batch.');
701
+ }
702
+ for (const entry of valid) entry.handle.ownership.value = NATIVE;
703
+ for (let index = 0; index < valid.length; index += 1) {
704
+ const handle = valid[index]?.handle;
705
+ const presentation = canonical[index];
706
+ if (!handle || !presentation) continue;
707
+ handle.activeAnimationId.value = 0;
708
+ handle.presentation.value = presentation;
709
+ handle.ownership.value = INTERACTIVE;
710
+ }
711
+ };
712
+
713
+ const animateOnUI = (
714
+ entries:
715
+ | readonly SmoothClipGroupMotionEntry[]
716
+ | readonly SmoothClipGroupKeyframeEntry[],
717
+ animation:
718
+ SmoothClipGroupMotionAnimation | SmoothClipGroupKeyframeAnimation
719
+ ): number => {
720
+ 'worklet';
721
+ if (disposed.value !== 0) return fail('Group driver was destroyed.');
722
+ if (!animationIsFinite(animation))
723
+ return fail('Group animation is invalid.');
724
+ const drivers = entries.map((entry) => entry.driver);
725
+ const valid = validateDrivers(drivers);
726
+ const targets: CanonicalSmoothClipPresentation[] = [];
727
+ for (const entry of entries) {
728
+ const target = canonicalizeClipPresentation(entry.target);
729
+ if (target === null) return fail('A group target is invalid.');
730
+ targets.push(target);
731
+ }
732
+ const starts = snapshotsForMotionStarts(
733
+ valid,
734
+ entries as readonly SmoothClipGroupMotionEntry[]
735
+ );
736
+ for (let index = 0; index < starts.length; index += 1) {
737
+ if (starts[index]?.clip.curve !== targets[index]?.clip.curve) {
738
+ return fail('Curve-changing animations must use streamed rendering.');
739
+ }
740
+ }
741
+
742
+ const native = requireNativeGroups();
743
+ const values: number[] = [];
744
+ let groupId = 0;
745
+ if (animation.type === 'keyframes') {
746
+ for (let index = 0; index < entries.length; index += 1) {
747
+ const entry = entries[index] as SmoothClipGroupKeyframeEntry;
748
+ const target = targets[index] as CanonicalSmoothClipPresentation;
749
+ const start = starts[index] as CanonicalSmoothClipPresentation;
750
+ const frames: Array<
751
+ Readonly<{
752
+ offset: number;
753
+ presentation: CanonicalSmoothClipPresentation;
754
+ }>
755
+ > = [];
756
+ let previousOffset = -1;
757
+ for (const frame of entry.frames) {
758
+ const canonical = canonicalizeClipPresentation(frame.presentation);
759
+ const presentation =
760
+ frames.length === 0 && entry.from === undefined
761
+ ? start
762
+ : canonical;
763
+ if (
764
+ canonical === null ||
765
+ presentation === null ||
766
+ !Number.isFinite(frame.offset) ||
767
+ frame.offset <= previousOffset ||
768
+ frame.offset < 0 ||
769
+ frame.offset > 1 ||
770
+ presentation.clip.curve !== target.clip.curve
771
+ ) {
772
+ return fail('Group keyframes are inconsistent.');
773
+ }
774
+ previousOffset = frame.offset;
775
+ frames.push({ offset: frame.offset, presentation });
776
+ }
777
+ if (
778
+ frames.length < 2 ||
779
+ frames[0]?.offset !== 0 ||
780
+ frames[frames.length - 1]?.offset !== 1 ||
781
+ !clipPresentationEquals(
782
+ frames[frames.length - 1]?.presentation ?? null,
783
+ target
784
+ ) ||
785
+ (entry.from !== undefined &&
786
+ !clipPresentationEquals(frames[0]?.presentation ?? null, start))
787
+ ) {
788
+ return fail('Group keyframes are inconsistent.');
789
+ }
790
+ values.push(valid[index]?.handle.driverId as number);
791
+ values.push(entry.from === undefined ? 0 : 1);
792
+ appendPresentation(values, start);
793
+ appendPresentation(values, target);
794
+ values.push(frames.length);
795
+ for (const frame of frames) {
796
+ values.push(frame.offset);
797
+ appendPresentation(values, frame.presentation);
798
+ }
799
+ }
800
+ groupId = native.animateKeyframesGroupV2(
801
+ controllerId,
802
+ values,
803
+ animation.duration,
804
+ reduceMotion,
805
+ suspensionPolicyCode(animation.suspensionPolicy),
806
+ animationStartTimestamp()
807
+ );
808
+ } else {
809
+ for (let index = 0; index < entries.length; index += 1) {
810
+ const entry = entries[index] as SmoothClipGroupMotionEntry;
811
+ values.push(valid[index]?.handle.driverId as number);
812
+ values.push(entry.from === undefined ? 0 : 1);
813
+ appendPresentation(
814
+ values,
815
+ starts[index] as CanonicalSmoothClipPresentation
816
+ );
817
+ appendPresentation(
818
+ values,
819
+ targets[index] as CanonicalSmoothClipPresentation
820
+ );
821
+ }
822
+ if (values.length !== entries.length * MOTION_ENTRY_STRIDE) {
823
+ return fail('Internal group wire encoding failed.');
824
+ }
825
+ if (animation.type === 'timing') {
826
+ const [x1, y1, x2, y2] = animation.controlPoints;
827
+ groupId = native.animateTimingGroupV2(
828
+ controllerId,
829
+ values,
830
+ animation.duration,
831
+ x1,
832
+ y1,
833
+ x2,
834
+ y2,
835
+ reduceMotion,
836
+ suspensionPolicyCode(animation.suspensionPolicy),
837
+ animationStartTimestamp()
838
+ );
839
+ } else {
840
+ if (!springScaleIsProvablyPositive(starts, targets, animation)) {
841
+ return fail(
842
+ 'This scale-changing spring is not provably positive; compile it to keyframes.'
843
+ );
844
+ }
845
+ const inheritVelocity =
846
+ animation.initialVelocity === undefined ||
847
+ animation.initialVelocity === 'inherit';
848
+ groupId = native.animateSpringGroupV2(
849
+ controllerId,
850
+ values,
851
+ animation.mass ?? 1,
852
+ animation.stiffness ?? 100,
853
+ animation.damping ?? 10,
854
+ inheritVelocity ? 0 : animation.initialVelocity,
855
+ inheritVelocity,
856
+ reduceMotion,
857
+ suspensionPolicyCode(animation.suspensionPolicy),
858
+ animationStartTimestamp()
859
+ );
860
+ }
861
+ }
862
+ if (!Number.isInteger(groupId) || groupId <= 0) {
863
+ return fail('Native rejected the complete group animation.');
864
+ }
865
+ registerNativeGroup(controllerId, groupId, drivers, targets);
866
+ return groupId;
867
+ };
868
+
869
+ const cancelOnUI = (
870
+ groupId: number,
871
+ behavior: SmoothClipGroupCancelBehavior = 'freeze'
872
+ ): readonly SmoothClipGroupSnapshot[] => {
873
+ 'worklet';
874
+ if (disposed.value !== 0) return fail('Group driver was destroyed.');
875
+ if (!Number.isSafeInteger(groupId) || groupId <= 0) {
876
+ return fail('A group ID must be a positive integer.');
877
+ }
878
+ if (behavior !== 'freeze' && behavior !== 'finish') {
879
+ return fail('Group cancel behavior is invalid.');
880
+ }
881
+ const records = runtimeGroupRecords();
882
+ const record = records[String(groupId)];
883
+ if (!record || record.controllerId !== controllerId) return [];
884
+ const values = requireNativeGroups().cancelAnimationGroupV2(
885
+ groupId,
886
+ behavior === 'finish' ? 1 : 0
887
+ );
888
+ if (values.length === 0) {
889
+ for (const driver of record.drivers) {
890
+ const handle = driver.__smoothClipHandle;
891
+ if (!handle || handle.activeAnimationId.value !== groupId) continue;
892
+ handle.activeAnimationId.value = 0;
893
+ handle.ownership.value = INTERACTIVE;
894
+ }
895
+ delete records[String(groupId)];
896
+ return [];
897
+ }
898
+ const snapshots = parseSnapshots(record.drivers, values).snapshots;
899
+ applyInteractiveSnapshots(snapshots);
900
+ delete records[String(groupId)];
901
+ return snapshots;
902
+ };
903
+
904
+ const group: SmoothClipGroupDriver = {
905
+ kind: 'group',
906
+ ui: {
907
+ beginInteraction(drivers) {
908
+ 'worklet';
909
+ if (isRNRuntime()) return uiOnly();
910
+ return beginOnUI(drivers);
911
+ },
912
+ snapshotCurrent(drivers) {
913
+ 'worklet';
914
+ if (isRNRuntime()) return uiOnly();
915
+ return snapshotOnUI(drivers);
916
+ },
917
+ setBatch(entries) {
918
+ 'worklet';
919
+ if (isRNRuntime()) return uiOnly();
920
+ setBatchOnUI(entries);
921
+ },
922
+ animateTo(entries, animation) {
923
+ 'worklet';
924
+ if (isRNRuntime()) return uiOnly();
925
+ return animateOnUI(entries, animation);
926
+ },
927
+ cancel(groupId, behavior = 'freeze') {
928
+ 'worklet';
929
+ if (isRNRuntime()) return uiOnly();
930
+ return cancelOnUI(groupId, behavior);
931
+ },
932
+ },
933
+ react: {
934
+ beginInteraction(drivers) {
935
+ const { requestId, promise } = createGroupRequest<
936
+ readonly SmoothClipGroupSnapshot[]
937
+ >(controllerId, undefined, true);
938
+ scheduleOnUI(() => {
939
+ 'worklet';
940
+ try {
941
+ const result = beginOnUI(drivers);
942
+ scheduleOnRN(
943
+ settleGroupRequest,
944
+ controllerId,
945
+ requestId,
946
+ true,
947
+ result,
948
+ ''
949
+ );
950
+ } catch (error) {
951
+ scheduleOnRN(
952
+ settleGroupRequest,
953
+ controllerId,
954
+ requestId,
955
+ false,
956
+ undefined,
957
+ errorMessage(error)
958
+ );
959
+ }
960
+ });
961
+ return promise;
962
+ },
963
+ snapshotCurrent(drivers) {
964
+ const { requestId, promise } =
965
+ createGroupRequest<readonly SmoothClipGroupSnapshot[]>(
966
+ controllerId
967
+ );
968
+ scheduleOnUI(() => {
969
+ 'worklet';
970
+ try {
971
+ const result = snapshotOnUI(drivers);
972
+ scheduleOnRN(
973
+ settleGroupRequest,
974
+ controllerId,
975
+ requestId,
976
+ true,
977
+ result,
978
+ ''
979
+ );
980
+ } catch (error) {
981
+ scheduleOnRN(
982
+ settleGroupRequest,
983
+ controllerId,
984
+ requestId,
985
+ false,
986
+ undefined,
987
+ errorMessage(error)
988
+ );
989
+ }
990
+ });
991
+ return promise;
992
+ },
993
+ setBatch(entries) {
994
+ const { requestId, promise } = createGroupRequest<void>(
995
+ controllerId,
996
+ { value: undefined },
997
+ true
998
+ );
999
+ scheduleOnUI(() => {
1000
+ 'worklet';
1001
+ try {
1002
+ setBatchOnUI(entries);
1003
+ scheduleOnRN(
1004
+ settleGroupRequest,
1005
+ controllerId,
1006
+ requestId,
1007
+ true,
1008
+ undefined,
1009
+ ''
1010
+ );
1011
+ } catch (error) {
1012
+ scheduleOnRN(
1013
+ settleGroupRequest,
1014
+ controllerId,
1015
+ requestId,
1016
+ false,
1017
+ undefined,
1018
+ errorMessage(error)
1019
+ );
1020
+ }
1021
+ });
1022
+ return promise;
1023
+ },
1024
+ animateTo(entries, animation) {
1025
+ const { requestId, promise } = createGroupRequest<number>(
1026
+ controllerId,
1027
+ { value: 0 },
1028
+ true
1029
+ );
1030
+ scheduleOnUI(() => {
1031
+ 'worklet';
1032
+ try {
1033
+ const result = animateOnUI(entries, animation);
1034
+ scheduleOnRN(
1035
+ settleGroupRequest,
1036
+ controllerId,
1037
+ requestId,
1038
+ true,
1039
+ result,
1040
+ ''
1041
+ );
1042
+ } catch (error) {
1043
+ scheduleOnRN(
1044
+ settleGroupRequest,
1045
+ controllerId,
1046
+ requestId,
1047
+ false,
1048
+ undefined,
1049
+ errorMessage(error)
1050
+ );
1051
+ }
1052
+ });
1053
+ return promise;
1054
+ },
1055
+ cancel(groupId, behavior = 'freeze') {
1056
+ const { requestId, promise } = createGroupRequest<
1057
+ readonly SmoothClipGroupSnapshot[]
1058
+ >(controllerId, undefined, true);
1059
+ scheduleOnUI(() => {
1060
+ 'worklet';
1061
+ try {
1062
+ const result = cancelOnUI(groupId, behavior);
1063
+ scheduleOnRN(
1064
+ settleGroupRequest,
1065
+ controllerId,
1066
+ requestId,
1067
+ true,
1068
+ result,
1069
+ ''
1070
+ );
1071
+ } catch (error) {
1072
+ scheduleOnRN(
1073
+ settleGroupRequest,
1074
+ controllerId,
1075
+ requestId,
1076
+ false,
1077
+ undefined,
1078
+ errorMessage(error)
1079
+ );
1080
+ }
1081
+ });
1082
+ return promise;
1083
+ },
1084
+ },
1085
+ };
1086
+ controllerIds.set(group, controllerId);
1087
+ groupRef.current = group;
1088
+ }
1089
+
1090
+ useEffect(() => {
1091
+ disposed.value = 0;
1092
+ const controllerId = controllerIdFor(groupRef.current);
1093
+ const state = stateRef.current;
1094
+ if (state === null) return undefined;
1095
+ state.effectGeneration += 1;
1096
+ const effectGeneration = state.effectGeneration;
1097
+ nativeGroupDriverStates.set(controllerId, state);
1098
+ let subscription: { remove(): void } | undefined;
1099
+ if (GROUPS_SUPPORTED) {
1100
+ try {
1101
+ subscription = nativeGroups.onClipGroupAnimationComplete?.((result) => {
1102
+ if (result.controllerId !== controllerId) return;
1103
+ scheduleOnUI(
1104
+ synchronizeGroupCompletionOnUI,
1105
+ result.controllerId,
1106
+ result.groupId,
1107
+ result.finished,
1108
+ result.driverIds
1109
+ );
1110
+ deliverNativeGroupCompletion(
1111
+ controllerId,
1112
+ result.groupId,
1113
+ result.finished
1114
+ );
1115
+ });
1116
+ } catch {
1117
+ // A V1 native module may expose neither the event hook nor a compatible
1118
+ // emitter facade. Unsupported setup is a no-op; UI methods still report
1119
+ // the normal capability error when invoked.
1120
+ }
1121
+ }
1122
+ return () => {
1123
+ try {
1124
+ subscription?.remove();
1125
+ } catch {
1126
+ // Older native event subscriptions are not guaranteed to support V2
1127
+ // teardown. Cleanup must remain safe across a new-JS/V1-native pair.
1128
+ }
1129
+ rejectGroupRequests(controllerId);
1130
+ scheduleOnUI(
1131
+ (ownerId: number, generation: number, gone: { value: number }) => {
1132
+ 'worklet';
1133
+ gone.value = 1;
1134
+ const records = runtimeGroupRecords();
1135
+ const native = GROUPS_SUPPORTED ? requireNativeGroups() : null;
1136
+ for (const key of Object.keys(records)) {
1137
+ const record = records[key];
1138
+ if (record?.controllerId !== ownerId) continue;
1139
+ const groupId = Number(key);
1140
+ const values = native?.cancelAnimationGroupV2(groupId, 0) ?? [];
1141
+ let snapshots: readonly SmoothClipGroupSnapshot[] = [];
1142
+ if (values.length !== 0) {
1143
+ try {
1144
+ snapshots = parseSnapshots(record.drivers, values).snapshots;
1145
+ } catch {
1146
+ snapshots = [];
1147
+ }
1148
+ }
1149
+ for (let index = 0; index < record.drivers.length; index += 1) {
1150
+ const handle = record.drivers[index]?.__smoothClipHandle;
1151
+ if (!handle || handle.activeAnimationId.value !== groupId)
1152
+ continue;
1153
+ const snapshot = snapshots[index];
1154
+ if (snapshot) handle.presentation.value = snapshot.presentation;
1155
+ handle.activeAnimationId.value = 0;
1156
+ handle.ownership.value = INTERACTIVE;
1157
+ }
1158
+ delete records[key];
1159
+ if (values.length !== 0) {
1160
+ scheduleOnRN(queueNativeTeardownCompletion, ownerId, groupId);
1161
+ }
1162
+ }
1163
+ scheduleOnRN(finalizeNativeGroupDriverTeardown, ownerId, generation);
1164
+ },
1165
+ controllerId,
1166
+ effectGeneration,
1167
+ disposed
1168
+ );
1169
+ };
1170
+ }, [disposed]);
1171
+
1172
+ return groupRef.current;
1173
+ }
1174
+
1175
+ // The public object intentionally has no internal fields. Keeping the
1176
+ // controller association in a WeakMap preserves that surface while allowing
1177
+ // StrictMode effect replays to reuse the same native callback owner.
1178
+ const controllerIds = new WeakMap<SmoothClipGroupDriver, number>();
1179
+
1180
+ function controllerIdFor(group: SmoothClipGroupDriver | null): number {
1181
+ if (group === null) return 0;
1182
+ const existing = controllerIds.get(group);
1183
+ if (existing !== undefined) return existing;
1184
+ const id = allocateDriverId();
1185
+ controllerIds.set(group, id);
1186
+ return id;
1187
+ }
1188
+
1189
+ export type { SmoothClipGroupAnimationResult };