@mks2508/mks-ui 0.2.1 → 0.3.2

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 (155) hide show
  1. package/dist/react-ui/hooks/Animation/UseAutoHeight.js +7 -7
  2. package/dist/react-ui/hooks/DOM/UseIsInView.js +3 -3
  3. package/dist/react-ui/hooks/Formatting/UseListFormat.d.ts +49 -0
  4. package/dist/react-ui/hooks/Formatting/UseListFormat.d.ts.map +1 -0
  5. package/dist/react-ui/hooks/Formatting/UseListFormat.js +105 -0
  6. package/dist/react-ui/hooks/State/UseControlledState.js +4 -4
  7. package/dist/react-ui/hooks/State/UseDataState.js +5 -5
  8. package/dist/react-ui/hooks/index.d.ts +2 -0
  9. package/dist/react-ui/hooks/index.d.ts.map +1 -1
  10. package/dist/react-ui/hooks/index.js +1 -0
  11. package/dist/react-ui/index.js +22 -1
  12. package/dist/react-ui/lib/get-strict-context.js +3 -3
  13. package/dist/react-ui/primitives/CountingNumber/index.js +3 -3
  14. package/dist/react-ui/primitives/Highlight/index.js +26 -26
  15. package/dist/react-ui/primitives/Slot/index.js +3 -3
  16. package/dist/react-ui/primitives/index.d.ts +1 -0
  17. package/dist/react-ui/primitives/index.d.ts.map +1 -1
  18. package/dist/react-ui/primitives/index.js +18 -0
  19. package/dist/react-ui/primitives/waapi/Morph/Morph.types.d.ts +76 -0
  20. package/dist/react-ui/primitives/waapi/Morph/Morph.types.d.ts.map +1 -0
  21. package/dist/react-ui/primitives/waapi/Morph/MorphContext.d.ts +11 -0
  22. package/dist/react-ui/primitives/waapi/Morph/MorphContext.d.ts.map +1 -0
  23. package/dist/react-ui/primitives/waapi/Morph/MorphContext.js +19 -0
  24. package/dist/react-ui/primitives/waapi/Morph/index.d.ts +23 -0
  25. package/dist/react-ui/primitives/waapi/Morph/index.d.ts.map +1 -0
  26. package/dist/react-ui/primitives/waapi/Morph/index.js +45 -0
  27. package/dist/react-ui/primitives/waapi/Morph/techniques/index.d.ts +12 -0
  28. package/dist/react-ui/primitives/waapi/Morph/techniques/index.d.ts.map +1 -0
  29. package/dist/react-ui/primitives/waapi/Morph/techniques/useCSSGridMorph.d.ts +38 -0
  30. package/dist/react-ui/primitives/waapi/Morph/techniques/useCSSGridMorph.d.ts.map +1 -0
  31. package/dist/react-ui/primitives/waapi/Morph/techniques/useCSSGridMorph.js +78 -0
  32. package/dist/react-ui/primitives/waapi/Morph/techniques/useFLIPClipPath.d.ts +23 -0
  33. package/dist/react-ui/primitives/waapi/Morph/techniques/useFLIPClipPath.d.ts.map +1 -0
  34. package/dist/react-ui/primitives/waapi/Morph/techniques/useFLIPClipPath.js +140 -0
  35. package/dist/react-ui/primitives/waapi/Morph/techniques/useViewTransitions.d.ts +28 -0
  36. package/dist/react-ui/primitives/waapi/Morph/techniques/useViewTransitions.d.ts.map +1 -0
  37. package/dist/react-ui/primitives/waapi/Morph/techniques/useViewTransitions.js +77 -0
  38. package/dist/react-ui/primitives/waapi/Morph/useMorph.d.ts +27 -0
  39. package/dist/react-ui/primitives/waapi/Morph/useMorph.d.ts.map +1 -0
  40. package/dist/react-ui/primitives/waapi/Morph/useMorph.js +86 -0
  41. package/dist/react-ui/primitives/waapi/Reorder/Reorder.types.d.ts +168 -0
  42. package/dist/react-ui/primitives/waapi/Reorder/Reorder.types.d.ts.map +1 -0
  43. package/dist/react-ui/primitives/waapi/Reorder/index.d.ts +25 -0
  44. package/dist/react-ui/primitives/waapi/Reorder/index.d.ts.map +1 -0
  45. package/dist/react-ui/primitives/waapi/Reorder/index.js +186 -0
  46. package/dist/react-ui/primitives/waapi/Reorder/useReorder.d.ts +26 -0
  47. package/dist/react-ui/primitives/waapi/Reorder/useReorder.d.ts.map +1 -0
  48. package/dist/react-ui/primitives/waapi/Reorder/useReorder.js +48 -0
  49. package/dist/react-ui/primitives/waapi/Reorder/useReorderPresence.d.ts +33 -0
  50. package/dist/react-ui/primitives/waapi/Reorder/useReorderPresence.d.ts.map +1 -0
  51. package/dist/react-ui/primitives/waapi/Reorder/useReorderPresence.js +137 -0
  52. package/dist/react-ui/primitives/waapi/Reorder/utils/separatorCoordination.d.ts +47 -0
  53. package/dist/react-ui/primitives/waapi/Reorder/utils/separatorCoordination.d.ts.map +1 -0
  54. package/dist/react-ui/primitives/waapi/Reorder/utils/separatorCoordination.js +72 -0
  55. package/dist/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.styles.d.ts +10 -0
  56. package/dist/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.styles.d.ts.map +1 -0
  57. package/dist/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.types.d.ts +74 -0
  58. package/dist/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.types.d.ts.map +1 -0
  59. package/dist/react-ui/primitives/waapi/SlidingNumber/index.d.ts +33 -0
  60. package/dist/react-ui/primitives/waapi/SlidingNumber/index.d.ts.map +1 -0
  61. package/dist/react-ui/primitives/waapi/SlidingNumber/index.js +354 -0
  62. package/dist/react-ui/primitives/waapi/SlidingText/SlidingText.styles.d.ts +25 -0
  63. package/dist/react-ui/primitives/waapi/SlidingText/SlidingText.styles.d.ts.map +1 -0
  64. package/dist/react-ui/primitives/waapi/SlidingText/SlidingText.types.d.ts +57 -0
  65. package/dist/react-ui/primitives/waapi/SlidingText/SlidingText.types.d.ts.map +1 -0
  66. package/dist/react-ui/primitives/waapi/SlidingText/index.d.ts +26 -0
  67. package/dist/react-ui/primitives/waapi/SlidingText/index.d.ts.map +1 -0
  68. package/dist/react-ui/primitives/waapi/SlidingText/index.js +105 -0
  69. package/dist/react-ui/primitives/waapi/core/animationConstants.d.ts +156 -0
  70. package/dist/react-ui/primitives/waapi/core/animationConstants.d.ts.map +1 -0
  71. package/dist/react-ui/primitives/waapi/core/animationConstants.js +180 -0
  72. package/dist/react-ui/primitives/waapi/core/index.d.ts +16 -0
  73. package/dist/react-ui/primitives/waapi/core/index.d.ts.map +1 -0
  74. package/dist/react-ui/primitives/waapi/core/index.js +5 -0
  75. package/dist/react-ui/primitives/waapi/core/types.d.ts +143 -0
  76. package/dist/react-ui/primitives/waapi/core/types.d.ts.map +1 -0
  77. package/dist/react-ui/primitives/waapi/core/useAnimationOrchestrator.d.ts +32 -0
  78. package/dist/react-ui/primitives/waapi/core/useAnimationOrchestrator.d.ts.map +1 -0
  79. package/dist/react-ui/primitives/waapi/core/useAnimationOrchestrator.js +322 -0
  80. package/dist/react-ui/primitives/waapi/core/useElementRegistry.d.ts +21 -0
  81. package/dist/react-ui/primitives/waapi/core/useElementRegistry.d.ts.map +1 -0
  82. package/dist/react-ui/primitives/waapi/core/useElementRegistry.js +65 -0
  83. package/dist/react-ui/primitives/waapi/core/useFLIPAnimation.d.ts +20 -0
  84. package/dist/react-ui/primitives/waapi/core/useFLIPAnimation.d.ts.map +1 -0
  85. package/dist/react-ui/primitives/waapi/core/useFLIPAnimation.js +99 -0
  86. package/dist/react-ui/primitives/waapi/core/usePositionCapture.d.ts +24 -0
  87. package/dist/react-ui/primitives/waapi/core/usePositionCapture.d.ts.map +1 -0
  88. package/dist/react-ui/primitives/waapi/core/usePositionCapture.js +75 -0
  89. package/dist/react-ui/primitives/waapi/index.d.ts +33 -0
  90. package/dist/react-ui/primitives/waapi/index.d.ts.map +1 -0
  91. package/dist/react-ui/primitives/waapi/index.js +18 -0
  92. package/dist/react-ui/ui/Accordion/index.js +3 -3
  93. package/dist/react-ui/ui/Button/index.js +8 -8
  94. package/dist/react-ui/ui/Combobox/index.js +2 -2
  95. package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts +35 -0
  96. package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts.map +1 -0
  97. package/dist/react-ui/ui/DataCard/DataCard.styles.js +114 -0
  98. package/dist/react-ui/ui/DataCard/DataCard.types.d.ts +135 -0
  99. package/dist/react-ui/ui/DataCard/DataCard.types.d.ts.map +1 -0
  100. package/dist/react-ui/ui/DataCard/index.d.ts +129 -0
  101. package/dist/react-ui/ui/DataCard/index.d.ts.map +1 -0
  102. package/dist/react-ui/ui/DataCard/index.js +276 -0
  103. package/dist/react-ui/ui/Menu/index.js +2 -2
  104. package/dist/react-ui/ui/Switch/index.js +3 -3
  105. package/dist/react-ui/ui/Tabs/index.js +3 -3
  106. package/dist/react-ui/ui/TextFlow/TextFlow.styles.d.ts +16 -0
  107. package/dist/react-ui/ui/TextFlow/TextFlow.styles.d.ts.map +1 -0
  108. package/dist/react-ui/ui/TextFlow/TextFlow.types.d.ts +101 -0
  109. package/dist/react-ui/ui/TextFlow/TextFlow.types.d.ts.map +1 -0
  110. package/dist/react-ui/ui/TextFlow/index.d.ts +26 -0
  111. package/dist/react-ui/ui/TextFlow/index.d.ts.map +1 -0
  112. package/dist/react-ui/ui/TextFlow/index.js +187 -0
  113. package/dist/react-ui/ui/index.d.ts +2 -0
  114. package/dist/react-ui/ui/index.d.ts.map +1 -1
  115. package/dist/react-ui/ui/index.js +3 -0
  116. package/package.json +6 -2
  117. package/src/react-ui/hooks/Formatting/UseListFormat.ts +134 -0
  118. package/src/react-ui/hooks/index.ts +3 -0
  119. package/src/react-ui/primitives/index.ts +3 -0
  120. package/src/react-ui/primitives/waapi/Morph/Morph.types.ts +106 -0
  121. package/src/react-ui/primitives/waapi/Morph/MorphContext.tsx +21 -0
  122. package/src/react-ui/primitives/waapi/Morph/index.tsx +56 -0
  123. package/src/react-ui/primitives/waapi/Morph/techniques/index.ts +12 -0
  124. package/src/react-ui/primitives/waapi/Morph/techniques/useCSSGridMorph.ts +88 -0
  125. package/src/react-ui/primitives/waapi/Morph/techniques/useFLIPClipPath.ts +175 -0
  126. package/src/react-ui/primitives/waapi/Morph/techniques/useViewTransitions.ts +86 -0
  127. package/src/react-ui/primitives/waapi/Morph/useMorph.ts +100 -0
  128. package/src/react-ui/primitives/waapi/Reorder/Reorder.types.ts +177 -0
  129. package/src/react-ui/primitives/waapi/Reorder/index.tsx +260 -0
  130. package/src/react-ui/primitives/waapi/Reorder/useReorder.ts +46 -0
  131. package/src/react-ui/primitives/waapi/Reorder/useReorderPresence.ts +208 -0
  132. package/src/react-ui/primitives/waapi/Reorder/utils/separatorCoordination.ts +104 -0
  133. package/src/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.styles.ts +14 -0
  134. package/src/react-ui/primitives/waapi/SlidingNumber/SlidingNumber.types.ts +84 -0
  135. package/src/react-ui/primitives/waapi/SlidingNumber/index.tsx +474 -0
  136. package/src/react-ui/primitives/waapi/SlidingText/SlidingText.styles.ts +32 -0
  137. package/src/react-ui/primitives/waapi/SlidingText/SlidingText.types.ts +69 -0
  138. package/src/react-ui/primitives/waapi/SlidingText/index.tsx +140 -0
  139. package/src/react-ui/primitives/waapi/core/animationConstants.ts +215 -0
  140. package/src/react-ui/primitives/waapi/core/index.ts +53 -0
  141. package/src/react-ui/primitives/waapi/core/types.ts +200 -0
  142. package/src/react-ui/primitives/waapi/core/useAnimationOrchestrator.ts +429 -0
  143. package/src/react-ui/primitives/waapi/core/useElementRegistry.ts +80 -0
  144. package/src/react-ui/primitives/waapi/core/useFLIPAnimation.ts +137 -0
  145. package/src/react-ui/primitives/waapi/core/usePositionCapture.ts +105 -0
  146. package/src/react-ui/primitives/waapi/index.ts +116 -0
  147. package/src/react-ui/styles/animations.css +369 -0
  148. package/src/react-ui/ui/DataCard/DataCard.styles.ts +150 -0
  149. package/src/react-ui/ui/DataCard/DataCard.types.ts +146 -0
  150. package/src/react-ui/ui/DataCard/index.tsx +406 -0
  151. package/src/react-ui/ui/TextFlow/TextFlow.styles.ts +36 -0
  152. package/src/react-ui/ui/TextFlow/TextFlow.types.ts +118 -0
  153. package/src/react-ui/ui/TextFlow/index.tsx +276 -0
  154. package/src/react-ui/ui/index.ts +4 -0
  155. /package/dist/react-ui/components/MorphingPopover/{morphing-popover.module-CgbYV_HS.css → morphing-popover.module-BycNI8nU.css} +0 -0
@@ -0,0 +1,429 @@
1
+ import { useRef, useCallback, useEffect } from 'react';
2
+ import { TIMING, TRANSFORMS, EFFECTS, EASINGS } from './animationConstants';
3
+ import { useElementRegistry } from './useElementRegistry';
4
+ import { usePositionCapture } from './usePositionCapture';
5
+ import { useFLIPAnimation } from './useFLIPAnimation';
6
+ import type {
7
+ IAnimationOrchestratorAPI,
8
+ IAnimationOrchestratorConfig,
9
+ IExitOptions,
10
+ IEnterOptions,
11
+ IOrchestratorState
12
+ } from './types';
13
+
14
+ /**
15
+ * Main animation orchestrator hook.
16
+ * Composes useElementRegistry, usePositionCapture, and useFLIPAnimation
17
+ * into a full FLIP exit/enter pipeline.
18
+ *
19
+ * Handles the full exit animation sequence:
20
+ * 1. Capture positions of remaining elements (FIRST)
21
+ * 2. Apply position:absolute to exiting element
22
+ * 3. Force synchronous reflow
23
+ * 4. Capture AFTER positions (LAST)
24
+ * 5. Calculate INVERT deltas and PLAY FLIP animations
25
+ * 6. Run exit animation (per-token stagger or whole-element)
26
+ * 7. Wait for all animations to complete
27
+ * 8. Cleanup and notify completion
28
+ *
29
+ * @param config - Optional orchestrator configuration
30
+ * @returns Full orchestrator API
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * const orchestrator = useAnimationOrchestrator({
35
+ * onExitComplete: (id) => removeFromList(id)
36
+ * });
37
+ *
38
+ * <div ref={el => orchestrator.registerElement('item-1', el)} />
39
+ *
40
+ * await orchestrator.startExit('item-1');
41
+ * ```
42
+ */
43
+ export function useAnimationOrchestrator(
44
+ config?: IAnimationOrchestratorConfig
45
+ ): IAnimationOrchestratorAPI {
46
+ const configRef = useRef(config);
47
+ useEffect(() => {
48
+ configRef.current = config;
49
+ }, [config]);
50
+
51
+ const stateRef = useRef<IOrchestratorState>({
52
+ animatingIds: new Set(),
53
+ positions: new Map(),
54
+ activeAnimations: new Map()
55
+ });
56
+
57
+ const registry = useElementRegistry();
58
+ const positions = usePositionCapture(registry.getAll, {
59
+ minDeltaPx: config?.minDeltaPx ?? TIMING.MIN_DELTA_PX
60
+ });
61
+ const flip = useFLIPAnimation();
62
+
63
+ const exitDuration = config?.exitDuration ?? TIMING.EXIT_DURATION;
64
+ const flipDuration = config?.flipDuration ?? TIMING.FLIP_DURATION;
65
+ const exitEasing = config?.exitEasing ?? EASINGS.EASE_IN_CUBIC;
66
+
67
+ const cancelAnimation = useCallback((id: string) => {
68
+ const animations = stateRef.current.activeAnimations.get(id);
69
+ if (animations) {
70
+ animations.forEach(anim => anim.cancel());
71
+ stateRef.current.activeAnimations.delete(id);
72
+ }
73
+ stateRef.current.animatingIds.delete(id);
74
+ flip.cancel(id);
75
+ }, [flip]);
76
+
77
+ const cancelAllAnimations = useCallback(() => {
78
+ stateRef.current.activeAnimations.forEach(animations => {
79
+ animations.forEach(anim => anim.cancel());
80
+ });
81
+ stateRef.current.activeAnimations.clear();
82
+ stateRef.current.animatingIds.clear();
83
+ flip.cancelAll();
84
+ }, [flip]);
85
+
86
+ const flipBehavior = config?.flipBehavior ?? 'all';
87
+ const exitPositionStrategy = config?.exitPositionStrategy ?? 'absolute-fixed';
88
+
89
+ /**
90
+ * Full FLIP exit implementation.
91
+ * Based on research from Paul Lewis (aerotwist.com), react-flip-move,
92
+ * and Framer Motion popLayout mode.
93
+ */
94
+ const startExit = useCallback(async (id: string, options?: IExitOptions): Promise<void> => {
95
+ const el = registry.get(id);
96
+
97
+ if (!el || stateRef.current.animatingIds.has(id)) {
98
+ return;
99
+ }
100
+
101
+ stateRef.current.animatingIds.add(id);
102
+
103
+ const duration = options?.duration ?? exitDuration;
104
+ const easing = options?.easing ?? exitEasing;
105
+
106
+ // ========================================
107
+ // STEP 1: FIRST - Capture ALL positions BEFORE any DOM/CSS changes
108
+ // CRITICAL: Must capture BEFORE setting data attributes!
109
+ // ========================================
110
+ const allElements = registry.getAll();
111
+ const beforePositions = new Map<string, DOMRect>();
112
+
113
+ const exitingRect = el.getBoundingClientRect();
114
+ beforePositions.set(id, exitingRect);
115
+
116
+ allElements.forEach((element, elemId) => {
117
+ if (elemId !== id) {
118
+ const rect = element.getBoundingClientRect();
119
+ if (rect.width > 0 && rect.height > 0) {
120
+ beforePositions.set(elemId, rect);
121
+ }
122
+ }
123
+ });
124
+
125
+ const parent = el.parentElement;
126
+
127
+ // Set data attributes (triggers CSS position:absolute)
128
+ el.dataset.reorderState = 'exiting';
129
+ if (exitPositionStrategy === 'absolute-fixed') {
130
+ el.dataset.exitPosition = 'absolute';
131
+ }
132
+
133
+ // ========================================
134
+ // STEP 2: Apply position:absolute to exiting element
135
+ // Calculate position relative to parent BEFORE it re-centers
136
+ // ========================================
137
+ const parentRectBefore = parent?.getBoundingClientRect() || { left: 0, top: 0 };
138
+ const absoluteLeft = exitingRect.left - parentRectBefore.left;
139
+ const absoluteTop = exitingRect.top - parentRectBefore.top;
140
+
141
+ if (exitPositionStrategy === 'absolute-fixed') {
142
+ el.style.position = 'absolute';
143
+ el.style.left = `${absoluteLeft}px`;
144
+ el.style.top = `${absoluteTop}px`;
145
+ el.style.width = `${exitingRect.width}px`;
146
+ el.style.height = `${exitingRect.height}px`;
147
+ el.style.margin = '0';
148
+ }
149
+
150
+ // Compensate for parent movement after absolute positioning
151
+ const parentRectAfter = parent?.getBoundingClientRect() || { left: 0, top: 0 };
152
+ const parentDeltaX = parentRectAfter.left - parentRectBefore.left;
153
+ const parentDeltaY = parentRectAfter.top - parentRectBefore.top;
154
+
155
+ if (Math.abs(parentDeltaX) > 0.5 || Math.abs(parentDeltaY) > 0.5) {
156
+ el.style.transform = `translate(${-parentDeltaX}px, ${-parentDeltaY}px)`;
157
+ }
158
+
159
+ // ========================================
160
+ // STEP 3: SYNCHRONOUS reflow via getBoundingClientRect
161
+ // ========================================
162
+ // (Forces layout recalculation before paint)
163
+
164
+ // ========================================
165
+ // STEP 4: LAST - Capture AFTER positions IMMEDIATELY (same frame)
166
+ // ========================================
167
+ const afterPositions = new Map<string, DOMRect>();
168
+ allElements.forEach((element, elemId) => {
169
+ if (elemId !== id) {
170
+ const rect = element.getBoundingClientRect();
171
+ if (rect.width > 0 && rect.height > 0) {
172
+ afterPositions.set(elemId, rect);
173
+ }
174
+ }
175
+ });
176
+
177
+ // ========================================
178
+ // STEP 5: INVERT + PLAY - Calculate deltas and animate siblings
179
+ // ========================================
180
+ const flipAnimations: Animation[] = [];
181
+
182
+ if (flipBehavior !== 'none') {
183
+ let siblingsToAnimate: string[] = [];
184
+
185
+ if (flipBehavior === 'all') {
186
+ siblingsToAnimate = [...afterPositions.keys()];
187
+ } else if (flipBehavior === 'siblings-after') {
188
+ // Sort by actual DOM order using compareDocumentPosition
189
+ const elementsWithIds = Array.from(allElements.entries());
190
+ elementsWithIds.sort((a, b) => {
191
+ const posA = a[1];
192
+ const posB = b[1];
193
+ const position = posA.compareDocumentPosition(posB);
194
+ return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
195
+ });
196
+
197
+ const sortedKeys = elementsWithIds.map(([k]) => k);
198
+ const exitingIndex = sortedKeys.indexOf(id);
199
+
200
+ if (exitingIndex !== -1) {
201
+ siblingsToAnimate = sortedKeys.slice(exitingIndex + 1).filter(k => afterPositions.has(k));
202
+ }
203
+ }
204
+
205
+ siblingsToAnimate.forEach(siblingId => {
206
+ const before = beforePositions.get(siblingId);
207
+ const after = afterPositions.get(siblingId);
208
+
209
+ if (!before || !after) return;
210
+
211
+ const deltaX = before.left - after.left;
212
+ const deltaY = before.top - after.top;
213
+
214
+ const isSignificant = Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1;
215
+
216
+ if (isSignificant) {
217
+ const siblingEl = allElements.get(siblingId);
218
+ if (siblingEl) {
219
+ siblingEl.dataset.reorderState = 'flipping';
220
+
221
+ // Apply initial transform SYNCHRONOUSLY via CSS
222
+ // element.animate() does NOT apply initial keyframe synchronously
223
+ siblingEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
224
+
225
+ const anim = siblingEl.animate([
226
+ { transform: `translate(${deltaX}px, ${deltaY}px)` },
227
+ { transform: 'none' }
228
+ ], {
229
+ duration: flipDuration,
230
+ easing: EASINGS.MATERIAL_DECELERATE,
231
+ fill: 'forwards'
232
+ });
233
+
234
+ flipAnimations.push(anim);
235
+
236
+ anim.finished.then(() => {
237
+ siblingEl.style.transform = '';
238
+ siblingEl.dataset.reorderState = 'idle';
239
+ }).catch(() => {
240
+ siblingEl.style.transform = '';
241
+ siblingEl.dataset.reorderState = 'idle';
242
+ });
243
+ }
244
+ }
245
+ });
246
+ }
247
+
248
+ // ========================================
249
+ // STEP 6: Animate exiting element (fade out)
250
+ // ========================================
251
+ const exitAnimations: Animation[] = [];
252
+ // FIX: Use .waapi-sliding-text-token (not .sliding-text-token)
253
+ const tokens = el.querySelectorAll('.waapi-sliding-text-token');
254
+
255
+ if (tokens.length > 0) {
256
+ tokens.forEach((token, index) => {
257
+ const delay = index * TIMING.EXIT_STAGGER;
258
+ const anim = (token as HTMLElement).animate([
259
+ { opacity: 1, transform: 'translateY(0) scale(1)', filter: 'blur(0px)' },
260
+ {
261
+ opacity: 0,
262
+ transform: `translateY(${TRANSFORMS.OFFSET_Y_EXIT}px) scale(${TRANSFORMS.SCALE_EXIT})`,
263
+ filter: `blur(${EFFECTS.BLUR_EXIT}px)`
264
+ }
265
+ ], {
266
+ duration,
267
+ easing,
268
+ delay,
269
+ fill: 'forwards'
270
+ });
271
+ exitAnimations.push(anim);
272
+ });
273
+ } else {
274
+ const anim = el.animate([
275
+ { opacity: 1, transform: 'scale(1)' },
276
+ { opacity: 0, transform: `scale(${TRANSFORMS.SCALE_EXIT})` }
277
+ ], {
278
+ duration,
279
+ easing,
280
+ fill: 'forwards'
281
+ });
282
+ exitAnimations.push(anim);
283
+ }
284
+
285
+ stateRef.current.activeAnimations.set(id, [...exitAnimations, ...flipAnimations]);
286
+
287
+ // ========================================
288
+ // STEP 7: Wait for all animations to complete
289
+ // ========================================
290
+ try {
291
+ await Promise.all([
292
+ ...exitAnimations.map(a => a.finished),
293
+ ...flipAnimations.map(a => a.finished)
294
+ ]);
295
+ } catch {
296
+ stateRef.current.animatingIds.delete(id);
297
+ stateRef.current.activeAnimations.delete(id);
298
+ return;
299
+ }
300
+
301
+ // ========================================
302
+ // STEP 8: Cleanup
303
+ // ========================================
304
+ stateRef.current.animatingIds.delete(id);
305
+ stateRef.current.activeAnimations.delete(id);
306
+
307
+ el.dataset.reorderState = 'completed';
308
+
309
+ if (exitPositionStrategy === 'absolute-fixed') {
310
+ el.style.removeProperty('position');
311
+ el.style.removeProperty('left');
312
+ el.style.removeProperty('top');
313
+ el.style.removeProperty('width');
314
+ el.style.removeProperty('height');
315
+ el.style.removeProperty('margin');
316
+ }
317
+
318
+ delete el.dataset.exitPosition;
319
+ registry.unregister(id);
320
+
321
+ options?.onComplete?.();
322
+ configRef.current?.onExitComplete?.(id);
323
+ }, [registry, exitDuration, exitEasing, flipDuration, flipBehavior, exitPositionStrategy]);
324
+
325
+ /**
326
+ * Start enter animation for an element.
327
+ *
328
+ * Architecture: CSS + WAAPI
329
+ * - Assumes element already has data-reorder-state="entering" (set by Reorder.tsx)
330
+ * - CSS [data-reorder-state="entering"] { opacity: 0; transform: translateY(-8px) scale(0.95); }
331
+ * - WAAPI animates FROM that state TO visible
332
+ * - Sets data-reorder-state="idle" on completion
333
+ */
334
+ const startEnter = useCallback(async (id: string, options?: IEnterOptions): Promise<void> => {
335
+ const el = registry.get(id);
336
+ if (!el) return;
337
+
338
+ // Fallback: set entering state if not already set
339
+ if (!el.dataset.reorderState) {
340
+ el.dataset.reorderState = 'entering';
341
+ }
342
+
343
+ const enterDuration = configRef.current?.enterDuration ?? TIMING.ENTER_DURATION;
344
+ const enterEasing = configRef.current?.enterEasing ?? EASINGS.MATERIAL_DECELERATE;
345
+
346
+ const duration = options?.duration ?? enterDuration;
347
+ const easing = options?.easing ?? enterEasing;
348
+ const stagger = options?.stagger ?? TIMING.ENTER_STAGGER;
349
+
350
+ // FIX: Use .waapi-sliding-text-token (not .sliding-text-token)
351
+ const tokens = el.querySelectorAll('.waapi-sliding-text-token');
352
+ const animations: Animation[] = [];
353
+
354
+ // Keyframes match CSS entering state -> visible
355
+ const fromKeyframe = {
356
+ opacity: 0,
357
+ transform: 'translateY(-8px) scale(0.95)'
358
+ };
359
+ const toKeyframe = {
360
+ opacity: 1,
361
+ transform: 'none'
362
+ };
363
+
364
+ if (tokens.length > 0) {
365
+ tokens.forEach((token, index) => {
366
+ const delay = index * stagger;
367
+ (token as HTMLElement).dataset.reorderIndex = String(index);
368
+
369
+ const anim = (token as HTMLElement).animate([
370
+ { ...fromKeyframe, filter: `blur(${EFFECTS.BLUR_ENTER}px)` },
371
+ { ...toKeyframe, filter: 'blur(0px)' }
372
+ ], {
373
+ duration,
374
+ easing,
375
+ delay,
376
+ fill: 'forwards'
377
+ });
378
+ animations.push(anim);
379
+ });
380
+ } else {
381
+ const anim = el.animate([fromKeyframe, toKeyframe], {
382
+ duration,
383
+ easing,
384
+ fill: 'forwards'
385
+ });
386
+ animations.push(anim);
387
+ }
388
+
389
+ try {
390
+ await Promise.all(animations.map(a => a.finished));
391
+ } catch {
392
+ el.dataset.reorderState = 'idle';
393
+ return;
394
+ }
395
+
396
+ el.dataset.reorderState = 'idle';
397
+
398
+ options?.onComplete?.();
399
+ configRef.current?.onEnterComplete?.(id);
400
+ }, [registry]);
401
+
402
+ const isAnimating = useCallback((id?: string): boolean => {
403
+ if (id) return stateRef.current.animatingIds.has(id);
404
+ return stateRef.current.animatingIds.size > 0;
405
+ }, []);
406
+
407
+ const capturePositions = useCallback((excludeIds?: Set<string>) => {
408
+ return positions.capture(excludeIds);
409
+ }, [positions]);
410
+
411
+ useEffect(() => {
412
+ return () => {
413
+ cancelAllAnimations();
414
+ };
415
+ }, [cancelAllAnimations]);
416
+
417
+ return {
418
+ registry,
419
+ positions,
420
+ flip,
421
+ registerElement: registry.register,
422
+ startExit,
423
+ startEnter,
424
+ isAnimating,
425
+ cancelAnimation,
426
+ cancelAllAnimations,
427
+ capturePositions
428
+ };
429
+ }
@@ -0,0 +1,80 @@
1
+ import { useRef, useCallback, useEffect } from 'react';
2
+ import type { IElementRegistryAPI, IElementRegistryCallbacks } from './types';
3
+
4
+ /**
5
+ * Hook for tracking DOM elements by ID.
6
+ * Extracted from the WAAPI animation system's element registration logic.
7
+ *
8
+ * @param callbacks - Optional lifecycle callbacks for register/unregister events
9
+ * @returns Registry API for managing elements by ID
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const registry = useElementRegistry({
14
+ * onRegister: (id) => console.log(`Registered: ${id}`),
15
+ * onUnregister: (id) => console.log(`Unregistered: ${id}`)
16
+ * });
17
+ *
18
+ * // In render:
19
+ * <div ref={el => registry.register('item-1', el)} />
20
+ * ```
21
+ */
22
+ export function useElementRegistry(
23
+ callbacks?: IElementRegistryCallbacks
24
+ ): IElementRegistryAPI {
25
+ const elementsRef = useRef<Map<string, HTMLElement>>(new Map());
26
+
27
+ const callbacksRef = useRef(callbacks);
28
+ useEffect(() => {
29
+ callbacksRef.current = callbacks;
30
+ }, [callbacks]);
31
+
32
+ const register = useCallback((id: string, el: HTMLElement | null) => {
33
+ if (el) {
34
+ elementsRef.current.set(id, el);
35
+ callbacksRef.current?.onRegister?.(id, el);
36
+ } else {
37
+ if (elementsRef.current.has(id)) {
38
+ elementsRef.current.delete(id);
39
+ callbacksRef.current?.onUnregister?.(id);
40
+ }
41
+ }
42
+ }, []);
43
+
44
+ const unregister = useCallback((id: string) => {
45
+ if (elementsRef.current.has(id)) {
46
+ elementsRef.current.delete(id);
47
+ callbacksRef.current?.onUnregister?.(id);
48
+ }
49
+ }, []);
50
+
51
+ const get = useCallback((id: string): HTMLElement | undefined => {
52
+ return elementsRef.current.get(id);
53
+ }, []);
54
+
55
+ const getAll = useCallback((): Map<string, HTMLElement> => {
56
+ return new Map(elementsRef.current);
57
+ }, []);
58
+
59
+ const has = useCallback((id: string): boolean => {
60
+ return elementsRef.current.has(id);
61
+ }, []);
62
+
63
+ const clear = useCallback(() => {
64
+ const ids = Array.from(elementsRef.current.keys());
65
+ elementsRef.current.clear();
66
+ ids.forEach(id => callbacksRef.current?.onUnregister?.(id));
67
+ }, []);
68
+
69
+ return {
70
+ register,
71
+ unregister,
72
+ get,
73
+ getAll,
74
+ has,
75
+ clear,
76
+ get size() {
77
+ return elementsRef.current.size;
78
+ }
79
+ };
80
+ }
@@ -0,0 +1,137 @@
1
+ import { useRef, useCallback, useEffect } from 'react';
2
+ import { TIMING, EASINGS } from './animationConstants';
3
+ import type { IFLIPAnimationAPI, IFLIPAnimationOptions, IFLIPDelta } from './types';
4
+
5
+ const DEFAULT_DURATION = TIMING.FLIP_DURATION;
6
+ const DEFAULT_EASING = EASINGS.SPRING_GENTLE;
7
+
8
+ /**
9
+ * Hook for executing FLIP (First-Last-Invert-Play) animations.
10
+ * Uses spring easing for natural motion with slight overshoot.
11
+ *
12
+ * @returns FLIP animation API with animate, cancel, and status methods
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const flip = useFLIPAnimation();
17
+ *
18
+ * // After calculating deltas:
19
+ * await flip.animateAll(elements, deltas, {
20
+ * duration: 300,
21
+ * onComplete: (id) => console.log(`${id} completed`)
22
+ * });
23
+ * ```
24
+ */
25
+ export function useFLIPAnimation(): IFLIPAnimationAPI {
26
+ const activeAnimationsRef = useRef<Map<string, Animation>>(new Map());
27
+ const animatingIdsRef = useRef<Set<string>>(new Set());
28
+
29
+ const animate = useCallback((
30
+ element: HTMLElement,
31
+ delta: IFLIPDelta,
32
+ options?: IFLIPAnimationOptions
33
+ ): Animation => {
34
+ const duration = options?.duration ?? DEFAULT_DURATION;
35
+ const easing = options?.easing ?? DEFAULT_EASING;
36
+
37
+ if (activeAnimationsRef.current.has(delta.id)) {
38
+ activeAnimationsRef.current.get(delta.id)?.cancel();
39
+ }
40
+
41
+ animatingIdsRef.current.add(delta.id);
42
+ options?.onStart?.(delta.id);
43
+
44
+ const animation = element.animate([
45
+ { transform: `translate3d(${delta.deltaX}px, ${delta.deltaY}px, 0)` },
46
+ { transform: 'translate3d(0, 0, 0)' }
47
+ ], {
48
+ duration,
49
+ easing
50
+ });
51
+
52
+ activeAnimationsRef.current.set(delta.id, animation);
53
+
54
+ animation.onfinish = () => {
55
+ animatingIdsRef.current.delete(delta.id);
56
+ activeAnimationsRef.current.delete(delta.id);
57
+ options?.onComplete?.(delta.id);
58
+ };
59
+
60
+ animation.oncancel = () => {
61
+ animatingIdsRef.current.delete(delta.id);
62
+ activeAnimationsRef.current.delete(delta.id);
63
+ };
64
+
65
+ return animation;
66
+ }, []);
67
+
68
+ const animateAll = useCallback(async (
69
+ elements: Map<string, HTMLElement>,
70
+ deltas: Map<string, IFLIPDelta>,
71
+ options?: IFLIPAnimationOptions
72
+ ): Promise<void> => {
73
+ const animations: Animation[] = [];
74
+
75
+ deltas.forEach((delta, id) => {
76
+ if (!delta.isSignificant) return;
77
+
78
+ const element = elements.get(id);
79
+ if (!element) return;
80
+
81
+ const anim = animate(element, delta, options);
82
+ animations.push(anim);
83
+ });
84
+
85
+ if (animations.length === 0) return;
86
+
87
+ await Promise.all(
88
+ animations.map(anim =>
89
+ anim.finished.catch(() => {})
90
+ )
91
+ );
92
+ }, [animate]);
93
+
94
+ const cancel = useCallback((id: string) => {
95
+ const animation = activeAnimationsRef.current.get(id);
96
+ if (animation) {
97
+ animation.cancel();
98
+ activeAnimationsRef.current.delete(id);
99
+ animatingIdsRef.current.delete(id);
100
+ }
101
+ }, []);
102
+
103
+ const cancelAll = useCallback(() => {
104
+ activeAnimationsRef.current.forEach(animation => animation.cancel());
105
+ activeAnimationsRef.current.clear();
106
+ animatingIdsRef.current.clear();
107
+ }, []);
108
+
109
+ const isAnimating = useCallback((id?: string): boolean => {
110
+ if (id) return animatingIdsRef.current.has(id);
111
+ return animatingIdsRef.current.size > 0;
112
+ }, []);
113
+
114
+ useEffect(() => {
115
+ return () => {
116
+ cancelAll();
117
+ };
118
+ }, [cancelAll]);
119
+
120
+ // Memoize return object to prevent cascading re-renders
121
+ // that would cancel animations via useEffect cleanup chains
122
+ const api = useRef<IFLIPAnimationAPI>({
123
+ animate,
124
+ animateAll,
125
+ cancel,
126
+ cancelAll,
127
+ isAnimating
128
+ });
129
+
130
+ api.current.animate = animate;
131
+ api.current.animateAll = animateAll;
132
+ api.current.cancel = cancel;
133
+ api.current.cancelAll = cancelAll;
134
+ api.current.isAnimating = isAnimating;
135
+
136
+ return api.current;
137
+ }