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,189 @@
1
+ import type { SkFont } from "@shopify/react-native-skia";
2
+ import type { EasingFunction, SharedValue } from "react-native-reanimated";
3
+
4
+ export interface TimingConfig {
5
+ /** Duration in milliseconds */
6
+ duration: number;
7
+ /** Easing function (t: 0→1) => 0→1 */
8
+ easing: EasingFunction;
9
+ }
10
+
11
+ export interface AnimationConfig {
12
+ /** Timing for layout transforms (position, width changes). Defaults to NumberFlow's 900ms deceleration curve. */
13
+ transformTiming?: TimingConfig;
14
+ /** Timing for digit spin/rolling. Falls back to transformTiming if unset. */
15
+ spinTiming?: TimingConfig;
16
+ /** Timing for enter/exit opacity. Defaults to 450ms ease-out. */
17
+ opacityTiming?: TimingConfig;
18
+ }
19
+
20
+ /**
21
+ * Controls digit spin direction:
22
+ * - `1`: always spin upward
23
+ * - `-1`: always spin downward
24
+ * - `0`: each digit takes the shortest path
25
+ */
26
+ export type Trend = -1 | 0 | 1;
27
+
28
+ /**
29
+ * Trend prop accepted by components: either a static direction
30
+ * or a function that receives (prevValue, nextValue) and returns the direction.
31
+ */
32
+ export type TrendProp = Trend | ((prev: number, next: number) => Trend);
33
+
34
+ /**
35
+ * Animation behavior props shared by all Flow components (NumberFlow, TimeFlow,
36
+ * SkiaNumberFlow, SkiaTimeFlow). Controls how digit transitions behave.
37
+ */
38
+ export interface AnimationBehaviorProps extends AnimationConfig {
39
+ /**
40
+ * Digit spin direction. When omitted, auto-detects from value changes:
41
+ * increasing values spin up, decreasing spin down.
42
+ * Pass `0` explicitly for shortest-path per-digit behavior.
43
+ */
44
+ trend?: TrendProp;
45
+
46
+ /** Set to false to disable animations and update instantly. Defaults to true. */
47
+ animated?: boolean;
48
+
49
+ /** When true (default), disables animations when the device's "Reduce Motion" setting is on. */
50
+ respectMotionPreference?: boolean;
51
+
52
+ /**
53
+ * When true, unchanged lower-significance digits spin through a full cycle
54
+ * during transitions, making the number appear to pass through intermediate values.
55
+ * Defaults to false.
56
+ */
57
+ continuous?: boolean;
58
+
59
+ /** Enable edge gradient fade masking on digit slots. Defaults to true. */
60
+ mask?: boolean;
61
+
62
+ /** Called when update animations begin */
63
+ onAnimationsStart?: () => void;
64
+
65
+ /** Called when all update animations complete */
66
+ onAnimationsFinish?: () => void;
67
+ }
68
+
69
+ /** A single character with a stable key from formatToParts RTL/LTR keying */
70
+ export interface KeyedPart {
71
+ /** Stable key: "integer:0" (ones), "integer:1" (tens), "fraction:0" (tenths), "decimal:0", etc. */
72
+ key: string;
73
+ /** Whether this is a rolling digit or a static symbol */
74
+ type: "digit" | "symbol";
75
+ /** The display character */
76
+ char: string;
77
+ /** 0-9 for digits, -1 for symbols */
78
+ digitValue: number;
79
+ }
80
+
81
+ export function digitPart(key: string, value: number): KeyedPart {
82
+ return { key, type: "digit", char: String(value), digitValue: value };
83
+ }
84
+
85
+ export function symbolPart(key: string, char: string): KeyedPart {
86
+ return { key, type: "symbol", char, digitValue: -1 };
87
+ }
88
+
89
+ export interface GlyphMetrics {
90
+ /** Advance width for each measurable character */
91
+ charWidths: Record<string, number>;
92
+ /** Maximum width among digits 0-9 (used for uniform digit slot sizing) */
93
+ maxDigitWidth: number;
94
+ /** Line height computed from font metrics: ceil(descent - ascent) */
95
+ lineHeight: number;
96
+ /** Font ascent (negative value — distance above baseline) */
97
+ ascent: number;
98
+ /** Font descent (positive value — distance below baseline) */
99
+ descent: number;
100
+ /**
101
+ * Per-character tight vertical bounds relative to the baseline.
102
+ * `top` is negative (above baseline), `bottom` is positive (below baseline).
103
+ * Used by the adaptive mask to avoid fading into visible glyph content.
104
+ */
105
+ charBounds: Record<string, { top: number; bottom: number }>;
106
+ }
107
+
108
+ export type TextAlign = "left" | "right" | "center";
109
+
110
+ /**
111
+ * Per-position digit constraint. `max` defines the highest value the
112
+ * digit can display (inclusive), creating a wheel of (max + 1) values.
113
+ * Example: { max: 5 } creates a 0-5 wheel (6 elements).
114
+ */
115
+ export interface DigitConstraint {
116
+ max: number;
117
+ }
118
+
119
+ /**
120
+ * Maps integer positions to their constraints.
121
+ * Position 0 = ones, 1 = tens, 2 = hundreds, etc.
122
+ * Positions not listed default to { max: 9 } (standard 0-9 wheel).
123
+ */
124
+ export type DigitsProp = Record<number, DigitConstraint>;
125
+
126
+ /** Value props — mutually exclusive: provide `value` (JS-driven) or `sharedValue` (worklet-driven), not both. */
127
+ type SkiaNumberFlowValueProps =
128
+ | {
129
+ /** JS-thread numeric value. Mutually exclusive with sharedValue. */
130
+ value: number;
131
+ /** Intl.NumberFormatOptions for value formatting */
132
+ format?: Intl.NumberFormatOptions;
133
+ /** Locale(s) for Intl.NumberFormat */
134
+ locales?: Intl.LocalesArgument;
135
+ sharedValue?: never;
136
+ }
137
+ | {
138
+ value?: never;
139
+ format?: never;
140
+ locales?: never;
141
+ /** Worklet-driven pre-formatted string. Mutually exclusive with value. */
142
+ sharedValue: SharedValue<string>;
143
+ };
144
+
145
+ interface SkiaNumberFlowBaseProps extends AnimationBehaviorProps {
146
+ /** SkFont instance from useFont(). Required — renders empty until font loads. */
147
+ font: SkFont | null;
148
+ /** Text color (Skia color string). Defaults to "#000000". */
149
+ color?: string;
150
+ /** X position within the Canvas. Defaults to 0. */
151
+ x?: number;
152
+ /** Y position within the Canvas (baseline). Defaults to 0. */
153
+ y?: number;
154
+ /** Available width for alignment calculations. Defaults to 0. */
155
+ width?: number;
156
+ /** Text alignment within the available width. Defaults to "left". */
157
+ textAlign?: TextAlign;
158
+ /** Static string prepended before the number */
159
+ prefix?: string;
160
+ /** Static string appended after the number */
161
+ suffix?: string;
162
+ /** Parent opacity (SharedValue for animation coordination) */
163
+ opacity?: SharedValue<number>;
164
+
165
+ /**
166
+ * Per-position digit constraints. Maps integer position (0=ones, 1=tens, ...)
167
+ * to { max: N } where N is the highest digit value (inclusive).
168
+ * Example: { 1: { max: 5 } } for a wheel where tens go 0-5.
169
+ */
170
+ digits?: DigitsProp;
171
+
172
+ /**
173
+ * Controls digit width during worklet-driven scrubbing (when sharedValue is active).
174
+ * Value between 0 and 1 representing the percentile between min and max digit width.
175
+ * - 0: use narrowest digit width (tightest, may clip wide digits)
176
+ * - 0.5: use average digit width
177
+ * - 1: use widest digit width (no clipping, but wider spacing)
178
+ * Defaults to 0.75 (75th percentile) for a good balance.
179
+ * Only affects digits; symbols like "." keep their natural width.
180
+ */
181
+ scrubDigitWidthPercentile?: number;
182
+ }
183
+
184
+ /**
185
+ * Props for SkiaNumberFlow.
186
+ * Value changes are auto-announced for screen reader users.
187
+ * For VoiceOver/TalkBack focus-based reading, set `accessibilityLabel` on the parent Canvas.
188
+ */
189
+ export type SkiaNumberFlowProps = SkiaNumberFlowBaseProps & SkiaNumberFlowValueProps;
@@ -0,0 +1,27 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { AccessibilityInfo } from "react-native";
3
+
4
+ /**
5
+ * Announces label changes to screen readers for Skia components.
6
+ *
7
+ * Skia renders inside <Canvas>, which is opaque to the accessibility tree.
8
+ * This hook auto-announces value changes when a screen reader is active
9
+ * so users get audio feedback. The first render is skipped to avoid
10
+ * announcing the initial value.
11
+ */
12
+ export function useAccessibilityAnnouncement(label: string | undefined): void {
13
+ const isFirstRender = useRef(true);
14
+
15
+ useEffect(() => {
16
+ if (isFirstRender.current) {
17
+ isFirstRender.current = false;
18
+ return;
19
+ }
20
+
21
+ if (!label) return;
22
+
23
+ AccessibilityInfo.isScreenReaderEnabled().then((enabled) => {
24
+ if (enabled) AccessibilityInfo.announceForAccessibility(label);
25
+ });
26
+ }, [label]);
27
+ }
@@ -0,0 +1,30 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
2
+ import { makeMutable, 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
+ export function useAnimatedX(
12
+ targetX: number,
13
+ exiting: boolean,
14
+ transformTiming: TimingConfig,
15
+ ): SharedValue<number> {
16
+ const [animatedX] = useState(() => makeMutable(targetX));
17
+ const prevXRef = useRef(targetX);
18
+
19
+ useLayoutEffect(() => {
20
+ if (!exiting && prevXRef.current !== targetX) {
21
+ prevXRef.current = targetX;
22
+ animatedX.value = withTiming(targetX, {
23
+ duration: transformTiming.duration,
24
+ easing: transformTiming.easing,
25
+ });
26
+ }
27
+ }, [targetX, exiting, transformTiming, animatedX]);
28
+
29
+ return animatedX;
30
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { CharLayout } from "./layout";
3
+ import type { TimingConfig } from "./types";
4
+
5
+ /**
6
+ * Fires animation lifecycle callbacks (start/finish) when the layout changes.
7
+ *
8
+ * Uses refs for callbacks to avoid stale closures in the setTimeout,
9
+ * and tracks previous layout to detect actual changes (not initial mount).
10
+ */
11
+ export function useAnimationLifecycle(
12
+ layout: CharLayout[],
13
+ timings: { spin: TimingConfig; opacity: TimingConfig; transform: TimingConfig },
14
+ onAnimationsStart?: () => void,
15
+ onAnimationsFinish?: () => void,
16
+ ): void {
17
+ // Keep callback refs fresh so the setTimeout always calls the latest version
18
+ const onStartRef = useRef(onAnimationsStart);
19
+ onStartRef.current = onAnimationsStart;
20
+
21
+ const onFinishRef = useRef(onAnimationsFinish);
22
+ onFinishRef.current = onAnimationsFinish;
23
+
24
+ // Track previous layout to distinguish real changes from initial mount
25
+ const prevLayoutRef = useRef(layout);
26
+ const prevLayoutLenRef = useRef(layout.length);
27
+ const animTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
28
+
29
+ useEffect(() => {
30
+ const layoutChanged = layout !== prevLayoutRef.current;
31
+ const bothNonEmpty = layout.length > 0 && prevLayoutLenRef.current > 0;
32
+
33
+ if (layoutChanged && bothNonEmpty) {
34
+ onStartRef.current?.();
35
+
36
+ if (animTimerRef.current) clearTimeout(animTimerRef.current);
37
+
38
+ const maxDur = Math.max(
39
+ timings.spin.duration,
40
+ timings.opacity.duration,
41
+ timings.transform.duration,
42
+ );
43
+
44
+ animTimerRef.current = setTimeout(() => onFinishRef.current?.(), maxDur);
45
+ }
46
+
47
+ prevLayoutRef.current = layout;
48
+ prevLayoutLenRef.current = layout.length;
49
+
50
+ return () => {
51
+ if (animTimerRef.current) clearTimeout(animTimerRef.current);
52
+ };
53
+ }, [layout, timings.spin, timings.opacity, timings.transform]);
54
+ }
@@ -0,0 +1,21 @@
1
+ import { useReducedMotion } from "react-native-reanimated";
2
+
3
+ /**
4
+ * Returns whether NumberFlow animations are currently enabled.
5
+ *
6
+ * Use this to conditionally render UI based on animation support:
7
+ * ```tsx
8
+ * const canAnimate = useCanAnimate();
9
+ * return canAnimate ? <NumberFlow value={42} /> : <Text>42</Text>;
10
+ * ```
11
+ *
12
+ * When `respectMotionPreference` is true (default), returns false
13
+ * if the device's "Reduce Motion" accessibility setting is on.
14
+ */
15
+ export function useCanAnimate(respectMotionPreference = true): boolean {
16
+ const reducedMotion = useReducedMotion();
17
+
18
+ if (!respectMotionPreference) return true;
19
+
20
+ return !reducedMotion;
21
+ }
@@ -0,0 +1,112 @@
1
+ import { useRef } from "react";
2
+ import type { KeyedPart, Trend } from "./types";
3
+ import { parseDigitPosition } from "./utils";
4
+
5
+ /**
6
+ * Pure algorithm: given previous and current keyed parts, determines which
7
+ * unchanged lower-significance digits should do a full-wheel continuous spin.
8
+ *
9
+ * Returns a new Map with incremented generation counters for those digits.
10
+ * The generation counter is what triggers the spin animation in useDigitAnimation.
11
+ */
12
+ export function computeContinuousGenerations(
13
+ prevParts: KeyedPart[],
14
+ currentParts: KeyedPart[],
15
+ prevGenerations: Map<string, number>,
16
+ ): Map<string, number> {
17
+ // Build lookup of previous digit values by key
18
+ const prevDigits = new Map<string, number>();
19
+ for (const part of prevParts) {
20
+ if (part.type === "digit") {
21
+ prevDigits.set(part.key, part.digitValue);
22
+ }
23
+ }
24
+
25
+ // Find the highest-significance position where the digit value changed
26
+ let maxChangedPos = -Infinity;
27
+
28
+ for (const part of currentParts) {
29
+ if (part.type !== "digit") continue;
30
+
31
+ const pos = parseDigitPosition(part.key);
32
+ if (pos === undefined) continue;
33
+
34
+ const prevValue = prevDigits.get(part.key);
35
+ const changed = prevValue !== undefined && prevValue !== part.digitValue;
36
+
37
+ if (changed && pos > maxChangedPos) {
38
+ maxChangedPos = pos;
39
+ }
40
+ }
41
+
42
+ // No digit changed — nothing to spin
43
+ if (maxChangedPos === -Infinity) return prevGenerations;
44
+
45
+ /**
46
+ * Increment the generation counter for each unchanged digit whose
47
+ * significance is lower than the most-significant changed digit.
48
+ * Only digits that existed in the previous render qualify — entering
49
+ * digits have no previous value and can't be "unchanged".
50
+ */
51
+ const nextGenerations = new Map(prevGenerations);
52
+
53
+ for (const part of currentParts) {
54
+ if (part.type !== "digit") continue;
55
+
56
+ const pos = parseDigitPosition(part.key);
57
+ if (pos === undefined || pos >= maxChangedPos) continue;
58
+
59
+ const prevValue = prevDigits.get(part.key);
60
+ const unchanged = prevValue !== undefined && prevValue === part.digitValue;
61
+
62
+ if (unchanged) {
63
+ const prev = nextGenerations.get(part.key) ?? 0;
64
+ nextGenerations.set(part.key, prev + 1);
65
+ }
66
+ }
67
+
68
+ return nextGenerations;
69
+ }
70
+
71
+ /**
72
+ * Tracks which digits need a full-wheel continuous spin.
73
+ *
74
+ * When `continuous` is enabled, unchanged lower-significance digits spin
75
+ * through a complete cycle alongside higher-significance digits that changed.
76
+ * For example, 100 -> 200 with trend=1 makes ones and tens both do a
77
+ * full upward rotation even though their values didn't change.
78
+ *
79
+ * Returns a Map<key, generation> where generation increments each time
80
+ * a digit should perform a continuous spin. Returns undefined when
81
+ * continuous is disabled (zero overhead path).
82
+ */
83
+ export function useContinuousSpin(
84
+ keyedParts: KeyedPart[],
85
+ continuous: boolean | undefined,
86
+ trend: Trend,
87
+ ): Map<string, number> | undefined {
88
+ const prevPartsRef = useRef<KeyedPart[]>([]);
89
+ const generationsRef = useRef<Map<string, number>>(new Map());
90
+
91
+ if (!continuous) {
92
+ prevPartsRef.current = keyedParts;
93
+ return undefined;
94
+ }
95
+
96
+ const prevParts = prevPartsRef.current;
97
+ prevPartsRef.current = keyedParts;
98
+
99
+ // First render — nothing to compare against
100
+ if (prevParts.length === 0) return generationsRef.current;
101
+
102
+ // Shortest-path trend defeats the purpose of continuous spin
103
+ if (trend === 0) return generationsRef.current;
104
+
105
+ generationsRef.current = computeContinuousGenerations(
106
+ prevParts,
107
+ keyedParts,
108
+ generationsRef.current,
109
+ );
110
+
111
+ return generationsRef.current;
112
+ }
@@ -0,0 +1,93 @@
1
+ import { useState } from "react";
2
+ import {
3
+ makeMutable,
4
+ type SharedValue,
5
+ useAnimatedReaction,
6
+ withTiming,
7
+ } from "react-native-reanimated";
8
+ import { MAX_SLOTS } from "./constants";
9
+ import { localeDigitValue } from "./numerals";
10
+
11
+ const WIDTH_ANIM_MS = 200;
12
+
13
+ /**
14
+ * Creates a pool of SharedValue<number> that hold digit widths for worklet layout.
15
+ *
16
+ * **Tabular mode during scrubbing**: While sharedValue is active (scrubbing),
17
+ * all digits use a fixed width (scrubDigitWidth) — this eliminates jitter entirely
18
+ * because all digits occupy the same width regardless of which digit (0-9) is displayed.
19
+ * Only digits are affected; symbols (like `.`) keep their natural width.
20
+ *
21
+ * **Proportional mode when idle**: When scrubbing ends (sharedValue becomes
22
+ * empty), widths animate to proportional values for natural typography.
23
+ *
24
+ * This approach gives the best of both worlds: stable layout during rapid
25
+ * scrubbing, and beautiful proportional spacing for static display.
26
+ */
27
+ export function useDebouncedWidths(
28
+ digitWidths: number[] | null,
29
+ scrubDigitWidth: number,
30
+ sharedValue: SharedValue<string> | undefined,
31
+ prefix: string,
32
+ suffix: string,
33
+ zeroCodePoint = 48,
34
+ ): SharedValue<number>[] {
35
+ const [debouncedWidths] = useState(() =>
36
+ Array.from({ length: MAX_SLOTS }, () => makeMutable(-1)),
37
+ );
38
+ const [prevDigits] = useState(() => Array.from({ length: MAX_SLOTS }, () => makeMutable(-1)));
39
+ const [wasActive] = useState(() => makeMutable(false));
40
+
41
+ useAnimatedReaction(
42
+ () => sharedValue?.value ?? "",
43
+ (current, previous) => {
44
+ if (current === previous) return;
45
+ if (!digitWidths) return;
46
+
47
+ if (!current) {
48
+ // Scrubbing ended — animate to proportional widths
49
+ if (wasActive.value) {
50
+ wasActive.value = false;
51
+ for (let i = 0; i < MAX_SLOTS; i++) {
52
+ const prevDv = prevDigits[i].value;
53
+ if (prevDv >= 0) {
54
+ const proportionalWidth = digitWidths[prevDv];
55
+ debouncedWidths[i].value = withTiming(proportionalWidth, {
56
+ duration: WIDTH_ANIM_MS,
57
+ });
58
+ } else {
59
+ debouncedWidths[i].value = -1;
60
+ }
61
+ prevDigits[i].value = -1;
62
+ }
63
+ }
64
+ return;
65
+ }
66
+
67
+ const isFirstActivation = !wasActive.value;
68
+ if (isFirstActivation) wasActive.value = true;
69
+
70
+ const fullText = prefix + current + suffix;
71
+ const len = fullText.length;
72
+ let digitIndex = 0;
73
+
74
+ for (let i = 0; i < len && digitIndex < MAX_SLOTS; i++) {
75
+ const code = fullText.charCodeAt(i);
76
+ const dv = localeDigitValue(code, zeroCodePoint);
77
+ if (dv >= 0) {
78
+ prevDigits[digitIndex].value = dv;
79
+ debouncedWidths[digitIndex].value = scrubDigitWidth;
80
+ digitIndex++;
81
+ }
82
+ }
83
+
84
+ for (let i = digitIndex; i < MAX_SLOTS; i++) {
85
+ if (debouncedWidths[i].value !== -1) debouncedWidths[i].value = -1;
86
+ prevDigits[i].value = -1;
87
+ }
88
+ },
89
+ [prefix, suffix, digitWidths, scrubDigitWidth, zeroCodePoint],
90
+ );
91
+
92
+ return debouncedWidths;
93
+ }