number-flow-react-native 0.1.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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/lib/module/core/constants.js +21 -0
  4. package/lib/module/core/constants.js.map +1 -0
  5. package/lib/module/core/intlHelpers.js +310 -0
  6. package/lib/module/core/intlHelpers.js.map +1 -0
  7. package/lib/module/core/layout.js +71 -0
  8. package/lib/module/core/layout.js.map +1 -0
  9. package/lib/module/core/mask.js +50 -0
  10. package/lib/module/core/mask.js.map +1 -0
  11. package/lib/module/core/numerals/detection.js +105 -0
  12. package/lib/module/core/numerals/detection.js.map +1 -0
  13. package/lib/module/core/numerals/digits.js +128 -0
  14. package/lib/module/core/numerals/digits.js.map +1 -0
  15. package/lib/module/core/numerals/index.js +5 -0
  16. package/lib/module/core/numerals/index.js.map +1 -0
  17. package/lib/module/core/numerals/tables.js +114 -0
  18. package/lib/module/core/numerals/tables.js.map +1 -0
  19. package/lib/module/core/superscript.js +31 -0
  20. package/lib/module/core/superscript.js.map +1 -0
  21. package/lib/module/core/timeLayout.js +98 -0
  22. package/lib/module/core/timeLayout.js.map +1 -0
  23. package/lib/module/core/timeTypes.js +4 -0
  24. package/lib/module/core/timeTypes.js.map +1 -0
  25. package/lib/module/core/timing.js +45 -0
  26. package/lib/module/core/timing.js.map +1 -0
  27. package/lib/module/core/types.js +58 -0
  28. package/lib/module/core/types.js.map +1 -0
  29. package/lib/module/core/useAccessibilityAnnouncement.js +27 -0
  30. package/lib/module/core/useAccessibilityAnnouncement.js.map +1 -0
  31. package/lib/module/core/useAnimatedX.js +25 -0
  32. package/lib/module/core/useAnimatedX.js.map +1 -0
  33. package/lib/module/core/useAnimationLifecycle.js +37 -0
  34. package/lib/module/core/useAnimationLifecycle.js.map +1 -0
  35. package/lib/module/core/useCanAnimate.js +22 -0
  36. package/lib/module/core/useCanAnimate.js.map +1 -0
  37. package/lib/module/core/useContinuousSpin.js +89 -0
  38. package/lib/module/core/useContinuousSpin.js.map +1 -0
  39. package/lib/module/core/useDebouncedWidths.js +74 -0
  40. package/lib/module/core/useDebouncedWidths.js.map +1 -0
  41. package/lib/module/core/useDigitAnimation.js +138 -0
  42. package/lib/module/core/useDigitAnimation.js.map +1 -0
  43. package/lib/module/core/useFlowPipeline.js +85 -0
  44. package/lib/module/core/useFlowPipeline.js.map +1 -0
  45. package/lib/module/core/useFormattedValue.js +28 -0
  46. package/lib/module/core/useFormattedValue.js.map +1 -0
  47. package/lib/module/core/useLayoutDiff.js +59 -0
  48. package/lib/module/core/useLayoutDiff.js.map +1 -0
  49. package/lib/module/core/useNumberFormatting.js +158 -0
  50. package/lib/module/core/useNumberFormatting.js.map +1 -0
  51. package/lib/module/core/useSlotOpacity.js +53 -0
  52. package/lib/module/core/useSlotOpacity.js.map +1 -0
  53. package/lib/module/core/useTimeFormatting.js +74 -0
  54. package/lib/module/core/useTimeFormatting.js.map +1 -0
  55. package/lib/module/core/useTimingResolution.js +21 -0
  56. package/lib/module/core/useTimingResolution.js.map +1 -0
  57. package/lib/module/core/useWorkletFormatting.js +49 -0
  58. package/lib/module/core/useWorkletFormatting.js.map +1 -0
  59. package/lib/module/core/utils.js +132 -0
  60. package/lib/module/core/utils.js.map +1 -0
  61. package/lib/module/core/warnings.js +10 -0
  62. package/lib/module/core/warnings.js.map +1 -0
  63. package/lib/module/index.js +7 -0
  64. package/lib/module/index.js.map +1 -0
  65. package/lib/module/native/DigitSlot.js +163 -0
  66. package/lib/module/native/DigitSlot.js.map +1 -0
  67. package/lib/module/native/NumberFlow.js +244 -0
  68. package/lib/module/native/NumberFlow.js.map +1 -0
  69. package/lib/module/native/SymbolSlot.js +52 -0
  70. package/lib/module/native/SymbolSlot.js.map +1 -0
  71. package/lib/module/native/TimeFlow.js +270 -0
  72. package/lib/module/native/TimeFlow.js.map +1 -0
  73. package/lib/module/native/index.js +5 -0
  74. package/lib/module/native/index.js.map +1 -0
  75. package/lib/module/native/renderSlots.js +108 -0
  76. package/lib/module/native/renderSlots.js.map +1 -0
  77. package/lib/module/native/types.js +4 -0
  78. package/lib/module/native/types.js.map +1 -0
  79. package/lib/module/native/useMeasuredGlyphMetrics.js +156 -0
  80. package/lib/module/native/useMeasuredGlyphMetrics.js.map +1 -0
  81. package/lib/module/package.json +1 -0
  82. package/lib/module/skia/DigitSlot.js +171 -0
  83. package/lib/module/skia/DigitSlot.js.map +1 -0
  84. package/lib/module/skia/SkiaNumberFlow.js +430 -0
  85. package/lib/module/skia/SkiaNumberFlow.js.map +1 -0
  86. package/lib/module/skia/SkiaTimeFlow.js +226 -0
  87. package/lib/module/skia/SkiaTimeFlow.js.map +1 -0
  88. package/lib/module/skia/SymbolSlot.js +92 -0
  89. package/lib/module/skia/SymbolSlot.js.map +1 -0
  90. package/lib/module/skia/index.js +6 -0
  91. package/lib/module/skia/index.js.map +1 -0
  92. package/lib/module/skia/renderSlots.js +131 -0
  93. package/lib/module/skia/renderSlots.js.map +1 -0
  94. package/lib/module/skia/useGlyphMetrics.js +72 -0
  95. package/lib/module/skia/useGlyphMetrics.js.map +1 -0
  96. package/lib/module/skia/useScrubbing.js +165 -0
  97. package/lib/module/skia/useScrubbing.js.map +1 -0
  98. package/lib/module/skia/useSkiaFont.js +23 -0
  99. package/lib/module/skia/useSkiaFont.js.map +1 -0
  100. package/lib/typescript/core/constants.d.ts +15 -0
  101. package/lib/typescript/core/constants.d.ts.map +1 -0
  102. package/lib/typescript/core/intlHelpers.d.ts +10 -0
  103. package/lib/typescript/core/intlHelpers.d.ts.map +1 -0
  104. package/lib/typescript/core/layout.d.ts +22 -0
  105. package/lib/typescript/core/layout.d.ts.map +1 -0
  106. package/lib/typescript/core/mask.d.ts +18 -0
  107. package/lib/typescript/core/mask.d.ts.map +1 -0
  108. package/lib/typescript/core/numerals/detection.d.ts +17 -0
  109. package/lib/typescript/core/numerals/detection.d.ts.map +1 -0
  110. package/lib/typescript/core/numerals/digits.d.ts +43 -0
  111. package/lib/typescript/core/numerals/digits.d.ts.map +1 -0
  112. package/lib/typescript/core/numerals/index.d.ts +3 -0
  113. package/lib/typescript/core/numerals/index.d.ts.map +1 -0
  114. package/lib/typescript/core/numerals/tables.d.ts +32 -0
  115. package/lib/typescript/core/numerals/tables.d.ts.map +1 -0
  116. package/lib/typescript/core/superscript.d.ts +16 -0
  117. package/lib/typescript/core/superscript.d.ts.map +1 -0
  118. package/lib/typescript/core/timeLayout.d.ts +19 -0
  119. package/lib/typescript/core/timeLayout.d.ts.map +1 -0
  120. package/lib/typescript/core/timeTypes.d.ts +80 -0
  121. package/lib/typescript/core/timeTypes.d.ts.map +1 -0
  122. package/lib/typescript/core/timing.d.ts +6 -0
  123. package/lib/typescript/core/timing.d.ts.map +1 -0
  124. package/lib/typescript/core/types.d.ts +165 -0
  125. package/lib/typescript/core/types.d.ts.map +1 -0
  126. package/lib/typescript/core/useAccessibilityAnnouncement.d.ts +10 -0
  127. package/lib/typescript/core/useAccessibilityAnnouncement.d.ts.map +1 -0
  128. package/lib/typescript/core/useAnimatedX.d.ts +9 -0
  129. package/lib/typescript/core/useAnimatedX.d.ts.map +1 -0
  130. package/lib/typescript/core/useAnimationLifecycle.d.ts +14 -0
  131. package/lib/typescript/core/useAnimationLifecycle.d.ts.map +1 -0
  132. package/lib/typescript/core/useCanAnimate.d.ts +14 -0
  133. package/lib/typescript/core/useCanAnimate.d.ts.map +1 -0
  134. package/lib/typescript/core/useContinuousSpin.d.ts +23 -0
  135. package/lib/typescript/core/useContinuousSpin.d.ts.map +1 -0
  136. package/lib/typescript/core/useDebouncedWidths.d.ts +17 -0
  137. package/lib/typescript/core/useDebouncedWidths.d.ts.map +1 -0
  138. package/lib/typescript/core/useDigitAnimation.d.ts +38 -0
  139. package/lib/typescript/core/useDigitAnimation.d.ts.map +1 -0
  140. package/lib/typescript/core/useFlowPipeline.d.ts +46 -0
  141. package/lib/typescript/core/useFlowPipeline.d.ts.map +1 -0
  142. package/lib/typescript/core/useFormattedValue.d.ts +14 -0
  143. package/lib/typescript/core/useFormattedValue.d.ts.map +1 -0
  144. package/lib/typescript/core/useLayoutDiff.d.ts +18 -0
  145. package/lib/typescript/core/useLayoutDiff.d.ts.map +1 -0
  146. package/lib/typescript/core/useNumberFormatting.d.ts +18 -0
  147. package/lib/typescript/core/useNumberFormatting.d.ts.map +1 -0
  148. package/lib/typescript/core/useSlotOpacity.d.ts +18 -0
  149. package/lib/typescript/core/useSlotOpacity.d.ts.map +1 -0
  150. package/lib/typescript/core/useTimeFormatting.d.ts +22 -0
  151. package/lib/typescript/core/useTimeFormatting.d.ts.map +1 -0
  152. package/lib/typescript/core/useTimingResolution.d.ts +13 -0
  153. package/lib/typescript/core/useTimingResolution.d.ts.map +1 -0
  154. package/lib/typescript/core/useWorkletFormatting.d.ts +14 -0
  155. package/lib/typescript/core/useWorkletFormatting.d.ts.map +1 -0
  156. package/lib/typescript/core/utils.d.ts +44 -0
  157. package/lib/typescript/core/utils.d.ts.map +1 -0
  158. package/lib/typescript/core/warnings.d.ts +2 -0
  159. package/lib/typescript/core/warnings.d.ts.map +1 -0
  160. package/lib/typescript/index.d.ts +8 -0
  161. package/lib/typescript/index.d.ts.map +1 -0
  162. package/lib/typescript/native/DigitSlot.d.ts +27 -0
  163. package/lib/typescript/native/DigitSlot.d.ts.map +1 -0
  164. package/lib/typescript/native/NumberFlow.d.ts +3 -0
  165. package/lib/typescript/native/NumberFlow.d.ts.map +1 -0
  166. package/lib/typescript/native/SymbolSlot.d.ts +19 -0
  167. package/lib/typescript/native/SymbolSlot.d.ts.map +1 -0
  168. package/lib/typescript/native/TimeFlow.d.ts +3 -0
  169. package/lib/typescript/native/TimeFlow.d.ts.map +1 -0
  170. package/lib/typescript/native/index.d.ts +3 -0
  171. package/lib/typescript/native/index.d.ts.map +1 -0
  172. package/lib/typescript/native/renderSlots.d.ts +31 -0
  173. package/lib/typescript/native/renderSlots.d.ts.map +1 -0
  174. package/lib/typescript/native/types.d.ts +36 -0
  175. package/lib/typescript/native/types.d.ts.map +1 -0
  176. package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts +8 -0
  177. package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts.map +1 -0
  178. package/lib/typescript/package.json +1 -0
  179. package/lib/typescript/skia/DigitSlot.d.ts +35 -0
  180. package/lib/typescript/skia/DigitSlot.d.ts.map +1 -0
  181. package/lib/typescript/skia/SkiaNumberFlow.d.ts +3 -0
  182. package/lib/typescript/skia/SkiaNumberFlow.d.ts.map +1 -0
  183. package/lib/typescript/skia/SkiaTimeFlow.d.ts +3 -0
  184. package/lib/typescript/skia/SkiaTimeFlow.d.ts.map +1 -0
  185. package/lib/typescript/skia/SymbolSlot.d.ts +26 -0
  186. package/lib/typescript/skia/SymbolSlot.d.ts.map +1 -0
  187. package/lib/typescript/skia/index.d.ts +6 -0
  188. package/lib/typescript/skia/index.d.ts.map +1 -0
  189. package/lib/typescript/skia/renderSlots.d.ts +40 -0
  190. package/lib/typescript/skia/renderSlots.d.ts.map +1 -0
  191. package/lib/typescript/skia/useGlyphMetrics.d.ts +16 -0
  192. package/lib/typescript/skia/useGlyphMetrics.d.ts.map +1 -0
  193. package/lib/typescript/skia/useScrubbing.d.ts +59 -0
  194. package/lib/typescript/skia/useScrubbing.d.ts.map +1 -0
  195. package/lib/typescript/skia/useSkiaFont.d.ts +13 -0
  196. package/lib/typescript/skia/useSkiaFont.d.ts.map +1 -0
  197. package/package.json +104 -0
  198. package/src/core/constants.ts +20 -0
  199. package/src/core/intlHelpers.ts +351 -0
  200. package/src/core/layout.ts +108 -0
  201. package/src/core/mask.ts +72 -0
  202. package/src/core/numerals/detection.ts +112 -0
  203. package/src/core/numerals/digits.ts +102 -0
  204. package/src/core/numerals/index.ts +9 -0
  205. package/src/core/numerals/tables.ts +112 -0
  206. package/src/core/superscript.ts +27 -0
  207. package/src/core/timeLayout.ts +119 -0
  208. package/src/core/timeTypes.ts +88 -0
  209. package/src/core/timing.ts +60 -0
  210. package/src/core/types.ts +189 -0
  211. package/src/core/useAccessibilityAnnouncement.ts +27 -0
  212. package/src/core/useAnimatedX.ts +30 -0
  213. package/src/core/useAnimationLifecycle.ts +54 -0
  214. package/src/core/useCanAnimate.ts +21 -0
  215. package/src/core/useContinuousSpin.ts +112 -0
  216. package/src/core/useDebouncedWidths.ts +93 -0
  217. package/src/core/useDigitAnimation.ts +192 -0
  218. package/src/core/useFlowPipeline.ts +126 -0
  219. package/src/core/useFormattedValue.ts +32 -0
  220. package/src/core/useLayoutDiff.ts +71 -0
  221. package/src/core/useNumberFormatting.ts +164 -0
  222. package/src/core/useSlotOpacity.ts +66 -0
  223. package/src/core/useTimeFormatting.ts +95 -0
  224. package/src/core/useTimingResolution.ts +47 -0
  225. package/src/core/useWorkletFormatting.ts +59 -0
  226. package/src/core/utils.ts +149 -0
  227. package/src/core/warnings.ts +8 -0
  228. package/src/index.ts +15 -0
  229. package/src/native/DigitSlot.tsx +203 -0
  230. package/src/native/NumberFlow.tsx +287 -0
  231. package/src/native/SymbolSlot.tsx +68 -0
  232. package/src/native/TimeFlow.tsx +287 -0
  233. package/src/native/index.ts +2 -0
  234. package/src/native/renderSlots.tsx +150 -0
  235. package/src/native/types.ts +40 -0
  236. package/src/native/useMeasuredGlyphMetrics.tsx +205 -0
  237. package/src/skia/DigitSlot.tsx +221 -0
  238. package/src/skia/SkiaNumberFlow.tsx +506 -0
  239. package/src/skia/SkiaTimeFlow.tsx +257 -0
  240. package/src/skia/SymbolSlot.tsx +120 -0
  241. package/src/skia/index.ts +5 -0
  242. package/src/skia/renderSlots.tsx +180 -0
  243. package/src/skia/useGlyphMetrics.ts +79 -0
  244. package/src/skia/useScrubbing.ts +223 -0
  245. package/src/skia/useSkiaFont.ts +25 -0
@@ -0,0 +1,192 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
2
+ import {
3
+ makeMutable,
4
+ runOnJS,
5
+ type SharedValue,
6
+ useAnimatedReaction,
7
+ withTiming,
8
+ } from "react-native-reanimated";
9
+ import { DIGIT_COUNT } from "./constants";
10
+ import type { TimingConfig, Trend } from "./types";
11
+ import { useSlotOpacity } from "./useSlotOpacity";
12
+ import { computeRollDelta } from "./utils";
13
+
14
+ /**
15
+ * We use makeMutable (via useState) instead of useSharedValue because
16
+ * useSharedValue's cleanup calls cancelAnimation, which kills in-flight
17
+ * animations when the component re-renders in StrictMode.
18
+ */
19
+
20
+ interface UseDigitAnimationParams {
21
+ digitValue: number;
22
+ entering: boolean;
23
+ exiting: boolean;
24
+ trend: Trend;
25
+ spinTiming: TimingConfig;
26
+ opacityTiming: TimingConfig;
27
+ exitKey?: string;
28
+ onExitComplete?: (key: string) => void;
29
+ workletDigitValue?: SharedValue<number>;
30
+ /** Number of values in this digit's wheel. Defaults to 10 (0-9). */
31
+ digitCount?: number;
32
+ /** Incrementing counter from useContinuousSpin; triggers a full-wheel rotation when changed. */
33
+ continuousSpinGeneration?: number;
34
+ }
35
+
36
+ interface UseDigitAnimationResult {
37
+ initialDigit: number;
38
+ animDelta: SharedValue<number>;
39
+ currentDigitSV: SharedValue<number>;
40
+ slotOpacity: SharedValue<number>;
41
+ }
42
+
43
+ /**
44
+ * Manages the digit rolling state machine: delta tracking, exit animations,
45
+ * prop-driven digit changes, and worklet-driven digit updates.
46
+ *
47
+ * Each renderer creates its own format-specific Y transforms and position
48
+ * reaction that reads animDelta + currentDigitSV from this hook.
49
+ */
50
+ export function useDigitAnimation({
51
+ digitValue,
52
+ entering,
53
+ exiting,
54
+ trend,
55
+ spinTiming,
56
+ opacityTiming,
57
+ exitKey,
58
+ onExitComplete,
59
+ workletDigitValue,
60
+ digitCount,
61
+ continuousSpinGeneration,
62
+ }: UseDigitAnimationParams): UseDigitAnimationResult {
63
+ const resolvedDigitCount = digitCount ?? DIGIT_COUNT;
64
+ const initialDigit = entering ? 0 : digitValue;
65
+ const prevDigitRef = useRef(initialDigit);
66
+
67
+ /**
68
+ * animDelta starts at the computed roll distance and animates toward 0.
69
+ * The per-digit position reaction computes c = currentDigit - animDelta
70
+ * and uses mod-10 arithmetic to position each digit individually.
71
+ */
72
+ const [animDelta] = useState(() => makeMutable(0));
73
+ const [currentDigitSV] = useState(() => makeMutable(initialDigit));
74
+
75
+ const handleExitingStart = useCallback(() => {
76
+ if (prevDigitRef.current !== 0) {
77
+ const delta = computeRollDelta(prevDigitRef.current, 0, trend, resolvedDigitCount);
78
+ prevDigitRef.current = 0;
79
+ currentDigitSV.value = 0;
80
+ animDelta.value = delta;
81
+ animDelta.value = withTiming(0, {
82
+ duration: spinTiming.duration,
83
+ easing: spinTiming.easing,
84
+ });
85
+ }
86
+ }, [trend, spinTiming, animDelta, currentDigitSV, resolvedDigitCount]);
87
+
88
+ const slotOpacity = useSlotOpacity({
89
+ entering,
90
+ exiting,
91
+ opacityTiming,
92
+ exitKey,
93
+ onExitComplete,
94
+ onExitingStart: handleExitingStart,
95
+ });
96
+
97
+ useLayoutEffect(() => {
98
+ const workletActive = workletDigitValue !== undefined && workletDigitValue.value >= 0;
99
+ if (!exiting && !workletActive && prevDigitRef.current !== digitValue) {
100
+ const delta = computeRollDelta(prevDigitRef.current, digitValue, trend, resolvedDigitCount);
101
+ prevDigitRef.current = digitValue;
102
+ currentDigitSV.value = digitValue;
103
+ animDelta.value = delta;
104
+ animDelta.value = withTiming(0, {
105
+ duration: spinTiming.duration,
106
+ easing: spinTiming.easing,
107
+ });
108
+ }
109
+ }, [
110
+ digitValue,
111
+ exiting,
112
+ workletDigitValue,
113
+ trend,
114
+ spinTiming,
115
+ animDelta,
116
+ currentDigitSV,
117
+ resolvedDigitCount,
118
+ ]);
119
+
120
+ /**
121
+ * Continuous spin: when the generation counter increments, this digit's
122
+ * value didn't change but a higher-significance digit did. Perform a
123
+ * full-wheel rotation so the digit appears to "carry" through.
124
+ *
125
+ * Example: 100 → 200 with trend=1 — the ones digit (0→0) spins through
126
+ * all 10 values upward while the hundreds digit rolls 1→2 normally.
127
+ */
128
+ const prevSpinGenRef = useRef(continuousSpinGeneration ?? 0);
129
+
130
+ useLayoutEffect(() => {
131
+ const currentGen = continuousSpinGeneration ?? 0;
132
+
133
+ // Generation unchanged — no continuous spin needed
134
+ if (currentGen === prevSpinGenRef.current) return;
135
+ prevSpinGenRef.current = currentGen;
136
+
137
+ // Don't spin digits that are entering or exiting — they animate via opacity
138
+ if (exiting || entering) return;
139
+
140
+ // Full wheel rotation: e.g. 10 * 1 = spin up 10, or 6 * -1 = spin down 6 (for s10)
141
+ const delta = resolvedDigitCount * trend;
142
+ if (delta === 0) return;
143
+
144
+ // Accumulate onto any in-flight animation, then ease back to 0
145
+ animDelta.value = animDelta.value + delta;
146
+ animDelta.value = withTiming(0, {
147
+ duration: spinTiming.duration,
148
+ easing: spinTiming.easing,
149
+ });
150
+ }, [
151
+ continuousSpinGeneration,
152
+ exiting,
153
+ entering,
154
+ trend,
155
+ spinTiming,
156
+ resolvedDigitCount,
157
+ animDelta,
158
+ ]);
159
+
160
+ const syncFromWorklet = useCallback((digit: number) => {
161
+ prevDigitRef.current = digit;
162
+ }, []);
163
+
164
+ useAnimatedReaction(
165
+ () => workletDigitValue?.value ?? -1,
166
+ (current, previous) => {
167
+ if (current < 0 || current === previous) return;
168
+
169
+ const prev = currentDigitSV.value;
170
+ if (prev === current) return;
171
+
172
+ const delta = computeRollDelta(prev, current, trend, resolvedDigitCount);
173
+ currentDigitSV.value = current;
174
+
175
+ /**
176
+ * Accumulate remaining animation delta (composite: 'accumulate').
177
+ * Safe to read animDelta.value here — we're on the UI thread.
178
+ */
179
+ animDelta.value = animDelta.value + delta;
180
+
181
+ animDelta.value = withTiming(0, {
182
+ duration: spinTiming.duration,
183
+ easing: spinTiming.easing,
184
+ });
185
+
186
+ runOnJS(syncFromWorklet)(current);
187
+ },
188
+ [workletDigitValue, spinTiming, trend],
189
+ );
190
+
191
+ return { initialDigit, animDelta, currentDigitSV, slotOpacity };
192
+ }
@@ -0,0 +1,126 @@
1
+ import { useMemo, useRef } from "react";
2
+ import type { CharLayout } from "./layout";
3
+ import { computeAdaptiveMaskHeights, type MaskHeights } from "./mask";
4
+ import type { GlyphMetrics, KeyedPart, TimingConfig, Trend, TrendProp } from "./types";
5
+ import { useAnimationLifecycle } from "./useAnimationLifecycle";
6
+ import { useContinuousSpin } from "./useContinuousSpin";
7
+ import { useLayoutDiff } from "./useLayoutDiff";
8
+ import { useTimingResolution } from "./useTimingResolution";
9
+ import { resolveTrend } from "./utils";
10
+
11
+ interface FlowPipelineInput {
12
+ keyedParts: KeyedPart[];
13
+
14
+ // The scalar used for trend detection:
15
+ // NumberFlow: the numeric `value`
16
+ // TimeFlow: `totalSeconds` (h*3600 + m*60 + s)
17
+ trendValue: number | undefined;
18
+
19
+ // Pre-computed layout — each component handles its own layout computation
20
+ // because Skia has special sharedValue layout paths
21
+ layout: CharLayout[];
22
+
23
+ // Metrics for adaptive mask computation
24
+ metrics: GlyphMetrics | null;
25
+
26
+ // AnimationBehaviorProps
27
+ animated?: boolean;
28
+ respectMotionPreference?: boolean;
29
+ spinTiming?: TimingConfig;
30
+ opacityTiming?: TimingConfig;
31
+ transformTiming?: TimingConfig;
32
+ trend?: TrendProp;
33
+ continuous?: boolean;
34
+ mask?: boolean;
35
+ onAnimationsStart?: () => void;
36
+ onAnimationsFinish?: () => void;
37
+ }
38
+
39
+ interface FlowPipelineOutput {
40
+ resolvedSpinTiming: TimingConfig;
41
+ resolvedOpacityTiming: TimingConfig;
42
+ resolvedTransformTiming: TimingConfig;
43
+ resolvedTrend: Trend;
44
+ spinGenerations: Map<string, number> | undefined;
45
+
46
+ prevMap: Map<string, CharLayout>;
47
+ isInitialRender: boolean;
48
+ exitingEntries: Map<string, CharLayout>;
49
+ onExitComplete: (key: string) => void;
50
+
51
+ accessibilityLabel: string | undefined;
52
+ adaptiveMask: MaskHeights;
53
+ }
54
+
55
+ const ZERO_MASK: MaskHeights = { top: 0, bottom: 0, expansionTop: 0, expansionBottom: 0 };
56
+
57
+ /**
58
+ * Shared data pipeline for all Flow components (NumberFlow, TimeFlow,
59
+ * SkiaNumberFlow, SkiaTimeFlow).
60
+ *
61
+ * Handles: timing resolution, trend tracking, continuous spin,
62
+ * animation lifecycle, layout diffing, accessibility label,
63
+ * and adaptive mask computation.
64
+ *
65
+ * Layout computation is left to callers because Skia components
66
+ * have additional sharedValue-driven layout paths.
67
+ */
68
+ export function useFlowPipeline(input: FlowPipelineInput): FlowPipelineOutput {
69
+ // 1. Timing resolution
70
+ const { resolvedSpinTiming, resolvedOpacityTiming, resolvedTransformTiming } =
71
+ useTimingResolution(
72
+ input.animated,
73
+ input.respectMotionPreference,
74
+ input.spinTiming,
75
+ input.opacityTiming,
76
+ input.transformTiming,
77
+ );
78
+
79
+ // 2. Trend tracking
80
+ const prevValueRef = useRef<number | undefined>(input.trendValue);
81
+ const resolvedTrend = resolveTrend(input.trend, prevValueRef.current, input.trendValue);
82
+ prevValueRef.current = input.trendValue;
83
+
84
+ // 3. Continuous spin
85
+ const spinGenerations = useContinuousSpin(input.keyedParts, input.continuous, resolvedTrend);
86
+
87
+ // 4. Animation lifecycle
88
+ useAnimationLifecycle(
89
+ input.layout,
90
+ { spin: resolvedSpinTiming, opacity: resolvedOpacityTiming, transform: resolvedTransformTiming },
91
+ input.onAnimationsStart,
92
+ input.onAnimationsFinish,
93
+ );
94
+
95
+ // 5. Layout diff
96
+ const { prevMap, isInitialRender, exitingEntries, onExitComplete } = useLayoutDiff(input.layout);
97
+
98
+ // 6. Accessibility label
99
+ const accessibilityLabel = useMemo(() => {
100
+ if (input.keyedParts.length === 0) return undefined;
101
+
102
+ return input.keyedParts.map((p) => p.char).join("");
103
+ }, [input.keyedParts]);
104
+
105
+ // 7. Adaptive mask
106
+ const resolvedMask = input.mask ?? true;
107
+ const adaptiveMask = useMemo(() => {
108
+ if (!input.metrics || !resolvedMask) return ZERO_MASK;
109
+
110
+ return computeAdaptiveMaskHeights(input.layout, exitingEntries, input.metrics);
111
+ }, [input.metrics, resolvedMask, input.layout, exitingEntries]);
112
+
113
+ return {
114
+ resolvedSpinTiming,
115
+ resolvedOpacityTiming,
116
+ resolvedTransformTiming,
117
+ resolvedTrend,
118
+ spinGenerations,
119
+ prevMap,
120
+ isInitialRender,
121
+ exitingEntries,
122
+ onExitComplete,
123
+ accessibilityLabel,
124
+ adaptiveMask,
125
+ };
126
+ }
@@ -0,0 +1,32 @@
1
+ import { useMemo } from "react";
2
+ import { getOrCreateFormatter } from "./intlHelpers";
3
+
4
+ /**
5
+ * Formats a numeric value to a display string using Intl.NumberFormat, with optional prefix/suffix.
6
+ * Useful for providing an accessibility label on a parent Canvas for Skia components:
7
+ *
8
+ * ```tsx
9
+ * const label = useFormattedValue(value, { style: "currency", currency: "USD" });
10
+ *
11
+ * <Canvas accessible accessibilityLabel={label}>
12
+ * <SkiaNumberFlow value={value} font={font} format={{ style: "currency", currency: "USD" }} />
13
+ * </Canvas>
14
+ * ```
15
+ */
16
+ export function useFormattedValue(
17
+ value: number | undefined,
18
+ format?: Intl.NumberFormatOptions,
19
+ locales?: Intl.LocalesArgument,
20
+ prefix?: string,
21
+ suffix?: string,
22
+ ): string | undefined {
23
+ // Serialize format/locales to a stable string — avoids re-runs when callers pass inline objects
24
+ const formatKey = useMemo(() => JSON.stringify([locales, format]), [locales, format]);
25
+
26
+ return useMemo(() => {
27
+ if (value === undefined) return undefined;
28
+ const formatter = getOrCreateFormatter(locales, format);
29
+ return `${prefix ?? ""}${formatter.format(value)}${suffix ?? ""}`;
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [value, prefix, suffix, formatKey]);
32
+ }
@@ -0,0 +1,71 @@
1
+ import { useCallback, useReducer, useRef } from "react";
2
+ import type { CharLayout } from "./layout";
3
+
4
+ interface LayoutDiffResult {
5
+ prevMap: Map<string, CharLayout>;
6
+ isInitialRender: boolean;
7
+ exitingEntries: Map<string, CharLayout>;
8
+ onExitComplete: (key: string) => void;
9
+ }
10
+
11
+ /**
12
+ * Manages enter/exit tracking for layout transitions. Diffs the current
13
+ * layout against the previous one and tracks exiting entries until their
14
+ * animations complete.
15
+ *
16
+ * The diff logic runs during render (not in useEffect) using a ref-based
17
+ * idempotent pattern for StrictMode safety.
18
+ */
19
+ export function useLayoutDiff(layout: CharLayout[]): LayoutDiffResult {
20
+ const prevLayoutRef = useRef<CharLayout[]>([]);
21
+ const exitingRef = useRef<Map<string, CharLayout>>(new Map());
22
+ const [, forceUpdate] = useReducer((n: number) => n + 1, 0);
23
+ const isFirstLayoutRef = useRef(true);
24
+
25
+ const lastDiffIdRef = useRef("");
26
+ const diffResultRef = useRef<{
27
+ prevMap: Map<string, CharLayout>;
28
+ isInitialRender: boolean;
29
+ }>({ prevMap: new Map(), isInitialRender: true });
30
+
31
+ const onExitComplete = useCallback((key: string) => {
32
+ exitingRef.current.delete(key);
33
+ forceUpdate();
34
+ }, []);
35
+
36
+ // Stable layout identity — fast string concat instead of map+join
37
+ let layoutId = "";
38
+ for (const s of layout) {
39
+ layoutId += `${s.key}:${s.digitValue}|`;
40
+ }
41
+
42
+ if (layoutId !== lastDiffIdRef.current) {
43
+ lastDiffIdRef.current = layoutId;
44
+
45
+ const currentKeys = new Set(layout.map((s) => s.key));
46
+ const prevMap = new Map(prevLayoutRef.current.map((s) => [s.key, s]));
47
+
48
+ for (const prev of prevLayoutRef.current) {
49
+ if (!currentKeys.has(prev.key) && !exitingRef.current.has(prev.key)) {
50
+ exitingRef.current.set(prev.key, prev);
51
+ }
52
+ }
53
+
54
+ for (const key of currentKeys) {
55
+ exitingRef.current.delete(key);
56
+ }
57
+
58
+ const isInitialRender = isFirstLayoutRef.current;
59
+ if (layout.length > 0) isFirstLayoutRef.current = false;
60
+
61
+ diffResultRef.current = { prevMap, isInitialRender };
62
+ prevLayoutRef.current = layout;
63
+ }
64
+
65
+ return {
66
+ prevMap: diffResultRef.current.prevMap,
67
+ isInitialRender: diffResultRef.current.isInitialRender,
68
+ exitingEntries: exitingRef.current,
69
+ onExitComplete,
70
+ };
71
+ }
@@ -0,0 +1,164 @@
1
+ import { useMemo } from "react";
2
+ import { getOrCreateFormatter, safeFormatToParts } from "./intlHelpers";
3
+ import { detectOutputZeroCodePoint, localeDigitValue } from "./numerals";
4
+ import type { KeyedPart } from "./types";
5
+
6
+ /**
7
+ * Transforms `Intl.NumberFormat.formatToParts()` output into a flat array of
8
+ * stably-keyed single-character parts. Integer digits are keyed right-to-left
9
+ * (ones = `integer:0`, tens = `integer:1`, etc.) so that the ones place always
10
+ * maps to the same React key regardless of digit count. Fraction digits are
11
+ * keyed left-to-right from the decimal point (`fraction:0` = tenths, etc.).
12
+ *
13
+ * This keying ensures correct animation behavior when digits are added/removed
14
+ * (e.g., 9→10: ones place spins 9→0, tens place enters as new digit).
15
+ */
16
+ export function formatToKeyedParts(
17
+ value: number,
18
+ formatter: Intl.NumberFormat,
19
+ locales: Intl.LocalesArgument | undefined,
20
+ prefix = "",
21
+ suffix = "",
22
+ ): KeyedPart[] {
23
+ const rawParts = safeFormatToParts(formatter, value, locales);
24
+
25
+ /**
26
+ * Detect the actual zero codepoint from the formatted output rather than
27
+ * trusting resolvedOptions().numberingSystem. Hermes may report "arab" but
28
+ * output Latin digits, or vice versa. Output-based detection is always correct.
29
+ */
30
+ const rawString = rawParts.map((p) => p.value).join("");
31
+ const zeroCP = detectOutputZeroCodePoint(rawString);
32
+
33
+ // Step 1: Flatten all parts into single characters with their source type
34
+ interface FlatChar {
35
+ sourceType: string;
36
+ char: string;
37
+ }
38
+
39
+ const flatChars: FlatChar[] = [];
40
+
41
+ for (const char of prefix) {
42
+ flatChars.push({ sourceType: "prefix", char });
43
+ }
44
+
45
+ for (const part of rawParts) {
46
+ // Merge signs into unified types.
47
+ // The fallback parser emits extended types (exponentMinusSign, etc.)
48
+ // that aren't in Intl.NumberFormatPart["type"], so we widen to string.
49
+ const partType = part.type as string;
50
+ const type =
51
+ partType === "minusSign" || partType === "plusSign"
52
+ ? "sign"
53
+ : partType === "exponentMinusSign" || partType === "exponentPlusSign"
54
+ ? "exponentSign"
55
+ : partType;
56
+
57
+ // Replace exponentSeparator "E" with ×10 display
58
+ if (type === "exponentSeparator") {
59
+ flatChars.push({ sourceType: "exponentSeparator", char: "\u00D7" });
60
+ flatChars.push({ sourceType: "exponentSeparator", char: "1" });
61
+ flatChars.push({ sourceType: "exponentSeparator", char: "0" });
62
+ continue;
63
+ }
64
+
65
+ // Flatten digit sequences into individual characters
66
+ if (type === "integer" || type === "fraction" || type === "exponentInteger") {
67
+ for (const char of part.value) {
68
+ flatChars.push({ sourceType: type, char });
69
+ }
70
+ } else {
71
+ flatChars.push({ sourceType: type, char: part.value });
72
+ }
73
+ }
74
+
75
+ for (const char of suffix) {
76
+ flatChars.push({ sourceType: "suffix", char });
77
+ }
78
+
79
+ // Step 2: Key characters — integers+groups RTL, everything else LTR
80
+ const result: KeyedPart[] = new Array(flatChars.length);
81
+ const counts: Record<string, number> = {};
82
+
83
+ const nextKey = (type: string) => `${type}:${(counts[type] = (counts[type] ?? -1) + 1)}`;
84
+
85
+ // Find all integer + group indices (they form one contiguous block to key RTL)
86
+ const integerGroupIndices: number[] = [];
87
+ for (let i = 0; i < flatChars.length; i++) {
88
+ if (flatChars[i].sourceType === "integer" || flatChars[i].sourceType === "group") {
89
+ integerGroupIndices.push(i);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Key integer digits and group separators RTL — rightmost integer digit
95
+ * gets "integer:0" (ones place), next gets "integer:1" (tens), etc.
96
+ */
97
+ for (let i = integerGroupIndices.length - 1; i >= 0; i--) {
98
+ const idx = integerGroupIndices[i];
99
+ const fc = flatChars[idx];
100
+ const isDigit = fc.sourceType === "integer";
101
+ result[idx] = {
102
+ key: nextKey(fc.sourceType),
103
+ type: isDigit ? "digit" : "symbol",
104
+ char: fc.char,
105
+ digitValue: isDigit ? localeDigitValue(fc.char.charCodeAt(0), zeroCP) : -1,
106
+ };
107
+ }
108
+
109
+ // Find all exponent integer indices (key RTL, same logic as mantissa integers)
110
+ const exponentIntegerIndices: number[] = [];
111
+ for (let i = 0; i < flatChars.length; i++) {
112
+ if (flatChars[i].sourceType === "exponentInteger") {
113
+ exponentIntegerIndices.push(i);
114
+ }
115
+ }
116
+
117
+ for (let i = exponentIntegerIndices.length - 1; i >= 0; i--) {
118
+ const idx = exponentIntegerIndices[i];
119
+ const fc = flatChars[idx];
120
+ result[idx] = {
121
+ key: nextKey("exponentInteger"),
122
+ type: "digit",
123
+ char: fc.char,
124
+ digitValue: localeDigitValue(fc.char.charCodeAt(0), zeroCP),
125
+ };
126
+ }
127
+
128
+ // Key everything else LTR (fraction, decimal, prefix, suffix, exponentSeparator, exponentSign, etc.)
129
+ for (let i = 0; i < flatChars.length; i++) {
130
+ if (result[i]) continue; // already keyed (integer, group, or exponentInteger)
131
+ const fc = flatChars[i];
132
+ const isDigit = fc.sourceType === "fraction";
133
+ result[i] = {
134
+ key: nextKey(fc.sourceType),
135
+ type: isDigit ? "digit" : "symbol",
136
+ char: fc.char,
137
+ digitValue: isDigit ? localeDigitValue(fc.char.charCodeAt(0), zeroCP) : -1,
138
+ };
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Hook that formats a numeric value into stably-keyed character parts.
146
+ * Uses Intl.NumberFormat.formatToParts() with RTL integer keying.
147
+ */
148
+ export function useNumberFormatting(
149
+ value: number | undefined,
150
+ format: Intl.NumberFormatOptions | undefined,
151
+ locales: Intl.LocalesArgument | undefined,
152
+ prefix: string,
153
+ suffix: string,
154
+ ): KeyedPart[] {
155
+ // Serialize format/locales to a stable string — avoids re-runs when callers pass inline objects
156
+ const formatKey = useMemo(() => JSON.stringify([locales, format]), [locales, format]);
157
+
158
+ return useMemo(() => {
159
+ if (value === undefined) return [];
160
+ const formatter = getOrCreateFormatter(locales, format);
161
+ return formatToKeyedParts(value, formatter, locales, prefix, suffix);
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps
163
+ }, [value, prefix, suffix, formatKey]);
164
+ }
@@ -0,0 +1,66 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
2
+ import { makeMutable, runOnJS, type SharedValue, withTiming } from "react-native-reanimated";
3
+ import type { TimingConfig } from "./types";
4
+
5
+ /**
6
+ * We use makeMutable (via useState) instead of useSharedValue because
7
+ * useSharedValue's cleanup calls cancelAnimation, which kills in-flight
8
+ * animations when the component re-renders in StrictMode.
9
+ */
10
+
11
+ interface UseSlotOpacityParams {
12
+ entering: boolean;
13
+ exiting: boolean;
14
+ opacityTiming: TimingConfig;
15
+ exitKey?: string;
16
+ onExitComplete?: (key: string) => void;
17
+ onExitingStart?: () => void;
18
+ }
19
+
20
+ export function useSlotOpacity({
21
+ entering,
22
+ exiting,
23
+ opacityTiming,
24
+ exitKey,
25
+ onExitComplete,
26
+ onExitingStart,
27
+ }: UseSlotOpacityParams): SharedValue<number> {
28
+ const [slotOpacity] = useState(() => makeMutable(entering ? 0 : 1));
29
+ const prevStateRef = useRef<"entering" | "exiting" | "active" | null>(null);
30
+ const currentState = entering ? "entering" : exiting ? "exiting" : "active";
31
+
32
+ useLayoutEffect(() => {
33
+ if (currentState === prevStateRef.current) return;
34
+ const wasInitial = prevStateRef.current === null;
35
+ prevStateRef.current = currentState;
36
+
37
+ if (currentState === "entering") {
38
+ slotOpacity.value = withTiming(1, {
39
+ duration: opacityTiming.duration,
40
+ easing: opacityTiming.easing,
41
+ });
42
+ } else if (currentState === "exiting") {
43
+ slotOpacity.value = withTiming(
44
+ 0,
45
+ {
46
+ duration: opacityTiming.duration,
47
+ easing: opacityTiming.easing,
48
+ },
49
+ (finished) => {
50
+ "worklet";
51
+ if (finished && onExitComplete && exitKey) {
52
+ runOnJS(onExitComplete)(exitKey);
53
+ }
54
+ },
55
+ );
56
+ onExitingStart?.();
57
+ } else if (!wasInitial) {
58
+ slotOpacity.value = withTiming(1, {
59
+ duration: opacityTiming.duration,
60
+ easing: opacityTiming.easing,
61
+ });
62
+ }
63
+ }, [currentState, opacityTiming, exitKey, onExitComplete, onExitingStart, slotOpacity]);
64
+
65
+ return slotOpacity;
66
+ }