@react-native-motion-kit/text-motion 0.0.0-stage → 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 (252) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +20 -0
  3. package/README.ko.md +698 -0
  4. package/README.md +696 -2
  5. package/lib/module/accessibility/index.js +4 -0
  6. package/lib/module/accessibility/index.js.map +1 -0
  7. package/lib/module/accessibility/policies.js +39 -0
  8. package/lib/module/accessibility/policies.js.map +1 -0
  9. package/lib/module/controls/brand.js +4 -0
  10. package/lib/module/controls/brand.js.map +1 -0
  11. package/lib/module/controls/controls.js +28 -0
  12. package/lib/module/controls/controls.js.map +1 -0
  13. package/lib/module/controls/descriptors.js +55 -0
  14. package/lib/module/controls/descriptors.js.map +1 -0
  15. package/lib/module/controls/index.js +4 -0
  16. package/lib/module/controls/index.js.map +1 -0
  17. package/lib/module/effects/compose.js +48 -0
  18. package/lib/module/effects/compose.js.map +1 -0
  19. package/lib/module/effects/fade.js +15 -0
  20. package/lib/module/effects/fade.js.map +1 -0
  21. package/lib/module/effects/index.js +12 -0
  22. package/lib/module/effects/index.js.map +1 -0
  23. package/lib/module/effects/lineReveal.js +17 -0
  24. package/lib/module/effects/lineReveal.js.map +1 -0
  25. package/lib/module/effects/pulse.js +14 -0
  26. package/lib/module/effects/pulse.js.map +1 -0
  27. package/lib/module/effects/rise.js +14 -0
  28. package/lib/module/effects/rise.js.map +1 -0
  29. package/lib/module/effects/scale.js +15 -0
  30. package/lib/module/effects/scale.js.map +1 -0
  31. package/lib/module/effects/shake.js +14 -0
  32. package/lib/module/effects/shake.js.map +1 -0
  33. package/lib/module/effects/slide.js +15 -0
  34. package/lib/module/effects/slide.js.map +1 -0
  35. package/lib/module/effects/typewriter.js +19 -0
  36. package/lib/module/effects/typewriter.js.map +1 -0
  37. package/lib/module/effects/validation.js +10 -0
  38. package/lib/module/effects/validation.js.map +1 -0
  39. package/lib/module/index.js +10 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/presets/index.js +35 -0
  43. package/lib/module/presets/index.js.map +1 -0
  44. package/lib/module/recipe/component.js +60 -0
  45. package/lib/module/recipe/component.js.map +1 -0
  46. package/lib/module/recipe/defineTextMotion.js +9 -0
  47. package/lib/module/recipe/defineTextMotion.js.map +1 -0
  48. package/lib/module/recipe/descriptors.js +60 -0
  49. package/lib/module/recipe/descriptors.js.map +1 -0
  50. package/lib/module/recipe/index.js +5 -0
  51. package/lib/module/recipe/index.js.map +1 -0
  52. package/lib/module/recipe/recipe.js +163 -0
  53. package/lib/module/recipe/recipe.js.map +1 -0
  54. package/lib/module/renderers/index.js +5 -0
  55. package/lib/module/renderers/index.js.map +1 -0
  56. package/lib/module/renderers/nativeText.js +494 -0
  57. package/lib/module/renderers/nativeText.js.map +1 -0
  58. package/lib/module/renderers/nativeTextControls.js +96 -0
  59. package/lib/module/renderers/nativeTextControls.js.map +1 -0
  60. package/lib/module/renderers/nativeTextLineLayout.js +290 -0
  61. package/lib/module/renderers/nativeTextLineLayout.js.map +1 -0
  62. package/lib/module/renderers/nativeTextPlayback.js +91 -0
  63. package/lib/module/renderers/nativeTextPlayback.js.map +1 -0
  64. package/lib/module/renderers/overlayText.js +543 -0
  65. package/lib/module/renderers/overlayText.js.map +1 -0
  66. package/lib/module/renderers/overlayTextLayout.js +182 -0
  67. package/lib/module/renderers/overlayTextLayout.js.map +1 -0
  68. package/lib/module/renderers/rendererMotion.js +264 -0
  69. package/lib/module/renderers/rendererMotion.js.map +1 -0
  70. package/lib/module/split/createToken.js +19 -0
  71. package/lib/module/split/createToken.js.map +1 -0
  72. package/lib/module/split/custom.js +57 -0
  73. package/lib/module/split/custom.js.map +1 -0
  74. package/lib/module/split/graphemes.js +25 -0
  75. package/lib/module/split/graphemes.js.map +1 -0
  76. package/lib/module/split/index.js +7 -0
  77. package/lib/module/split/index.js.map +1 -0
  78. package/lib/module/split/lines.js +43 -0
  79. package/lib/module/split/lines.js.map +1 -0
  80. package/lib/module/split/segmenter.js +74 -0
  81. package/lib/module/split/segmenter.js.map +1 -0
  82. package/lib/module/split/words.js +28 -0
  83. package/lib/module/split/words.js.map +1 -0
  84. package/lib/module/timeline/index.js +7 -0
  85. package/lib/module/timeline/index.js.map +1 -0
  86. package/lib/module/timeline/parallel.js +21 -0
  87. package/lib/module/timeline/parallel.js.map +1 -0
  88. package/lib/module/timeline/sequence.js +18 -0
  89. package/lib/module/timeline/sequence.js.map +1 -0
  90. package/lib/module/timeline/stagger.js +43 -0
  91. package/lib/module/timeline/stagger.js.map +1 -0
  92. package/lib/module/timeline/validation.js +24 -0
  93. package/lib/module/timeline/validation.js.map +1 -0
  94. package/lib/module/timeline/wave.js +25 -0
  95. package/lib/module/timeline/wave.js.map +1 -0
  96. package/lib/module/types/index.js +4 -0
  97. package/lib/module/types/index.js.map +1 -0
  98. package/lib/module/types/recipe.js +4 -0
  99. package/lib/module/types/recipe.js.map +1 -0
  100. package/lib/module/types/renderer.js +27 -0
  101. package/lib/module/types/renderer.js.map +1 -0
  102. package/lib/module/types/token.js +2 -0
  103. package/lib/module/types/token.js.map +1 -0
  104. package/lib/typescript/package.json +1 -0
  105. package/lib/typescript/src/accessibility/index.d.ts +2 -0
  106. package/lib/typescript/src/accessibility/index.d.ts.map +1 -0
  107. package/lib/typescript/src/accessibility/policies.d.ts +13 -0
  108. package/lib/typescript/src/accessibility/policies.d.ts.map +1 -0
  109. package/lib/typescript/src/controls/brand.d.ts +2 -0
  110. package/lib/typescript/src/controls/brand.d.ts.map +1 -0
  111. package/lib/typescript/src/controls/controls.d.ts +28 -0
  112. package/lib/typescript/src/controls/controls.d.ts.map +1 -0
  113. package/lib/typescript/src/controls/descriptors.d.ts +14 -0
  114. package/lib/typescript/src/controls/descriptors.d.ts.map +1 -0
  115. package/lib/typescript/src/controls/index.d.ts +3 -0
  116. package/lib/typescript/src/controls/index.d.ts.map +1 -0
  117. package/lib/typescript/src/effects/compose.d.ts +11 -0
  118. package/lib/typescript/src/effects/compose.d.ts.map +1 -0
  119. package/lib/typescript/src/effects/fade.d.ts +10 -0
  120. package/lib/typescript/src/effects/fade.d.ts.map +1 -0
  121. package/lib/typescript/src/effects/index.d.ts +10 -0
  122. package/lib/typescript/src/effects/index.d.ts.map +1 -0
  123. package/lib/typescript/src/effects/lineReveal.d.ts +11 -0
  124. package/lib/typescript/src/effects/lineReveal.d.ts.map +1 -0
  125. package/lib/typescript/src/effects/pulse.d.ts +8 -0
  126. package/lib/typescript/src/effects/pulse.d.ts.map +1 -0
  127. package/lib/typescript/src/effects/rise.d.ts +8 -0
  128. package/lib/typescript/src/effects/rise.d.ts.map +1 -0
  129. package/lib/typescript/src/effects/scale.d.ts +10 -0
  130. package/lib/typescript/src/effects/scale.d.ts.map +1 -0
  131. package/lib/typescript/src/effects/shake.d.ts +8 -0
  132. package/lib/typescript/src/effects/shake.d.ts.map +1 -0
  133. package/lib/typescript/src/effects/slide.d.ts +10 -0
  134. package/lib/typescript/src/effects/slide.d.ts.map +1 -0
  135. package/lib/typescript/src/effects/typewriter.d.ts +13 -0
  136. package/lib/typescript/src/effects/typewriter.d.ts.map +1 -0
  137. package/lib/typescript/src/effects/validation.d.ts +2 -0
  138. package/lib/typescript/src/effects/validation.d.ts.map +1 -0
  139. package/lib/typescript/src/index.d.ts +15 -0
  140. package/lib/typescript/src/index.d.ts.map +1 -0
  141. package/lib/typescript/src/presets/index.d.ts +10 -0
  142. package/lib/typescript/src/presets/index.d.ts.map +1 -0
  143. package/lib/typescript/src/recipe/component.d.ts +3 -0
  144. package/lib/typescript/src/recipe/component.d.ts.map +1 -0
  145. package/lib/typescript/src/recipe/defineTextMotion.d.ts +4 -0
  146. package/lib/typescript/src/recipe/defineTextMotion.d.ts.map +1 -0
  147. package/lib/typescript/src/recipe/descriptors.d.ts +15 -0
  148. package/lib/typescript/src/recipe/descriptors.d.ts.map +1 -0
  149. package/lib/typescript/src/recipe/index.d.ts +4 -0
  150. package/lib/typescript/src/recipe/index.d.ts.map +1 -0
  151. package/lib/typescript/src/recipe/recipe.d.ts +67 -0
  152. package/lib/typescript/src/recipe/recipe.d.ts.map +1 -0
  153. package/lib/typescript/src/renderers/index.d.ts +3 -0
  154. package/lib/typescript/src/renderers/index.d.ts.map +1 -0
  155. package/lib/typescript/src/renderers/nativeText.d.ts +13 -0
  156. package/lib/typescript/src/renderers/nativeText.d.ts.map +1 -0
  157. package/lib/typescript/src/renderers/nativeTextControls.d.ts +16 -0
  158. package/lib/typescript/src/renderers/nativeTextControls.d.ts.map +1 -0
  159. package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts +84 -0
  160. package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts.map +1 -0
  161. package/lib/typescript/src/renderers/nativeTextPlayback.d.ts +21 -0
  162. package/lib/typescript/src/renderers/nativeTextPlayback.d.ts.map +1 -0
  163. package/lib/typescript/src/renderers/overlayText.d.ts +13 -0
  164. package/lib/typescript/src/renderers/overlayText.d.ts.map +1 -0
  165. package/lib/typescript/src/renderers/overlayTextLayout.d.ts +36 -0
  166. package/lib/typescript/src/renderers/overlayTextLayout.d.ts.map +1 -0
  167. package/lib/typescript/src/renderers/rendererMotion.d.ts +53 -0
  168. package/lib/typescript/src/renderers/rendererMotion.d.ts.map +1 -0
  169. package/lib/typescript/src/split/createToken.d.ts +11 -0
  170. package/lib/typescript/src/split/createToken.d.ts.map +1 -0
  171. package/lib/typescript/src/split/custom.d.ts +17 -0
  172. package/lib/typescript/src/split/custom.d.ts.map +1 -0
  173. package/lib/typescript/src/split/graphemes.d.ts +4 -0
  174. package/lib/typescript/src/split/graphemes.d.ts.map +1 -0
  175. package/lib/typescript/src/split/index.d.ts +5 -0
  176. package/lib/typescript/src/split/index.d.ts.map +1 -0
  177. package/lib/typescript/src/split/lines.d.ts +4 -0
  178. package/lib/typescript/src/split/lines.d.ts.map +1 -0
  179. package/lib/typescript/src/split/segmenter.d.ts +18 -0
  180. package/lib/typescript/src/split/segmenter.d.ts.map +1 -0
  181. package/lib/typescript/src/split/words.d.ts +4 -0
  182. package/lib/typescript/src/split/words.d.ts.map +1 -0
  183. package/lib/typescript/src/timeline/index.d.ts +5 -0
  184. package/lib/typescript/src/timeline/index.d.ts.map +1 -0
  185. package/lib/typescript/src/timeline/parallel.d.ts +4 -0
  186. package/lib/typescript/src/timeline/parallel.d.ts.map +1 -0
  187. package/lib/typescript/src/timeline/sequence.d.ts +4 -0
  188. package/lib/typescript/src/timeline/sequence.d.ts.map +1 -0
  189. package/lib/typescript/src/timeline/stagger.d.ts +11 -0
  190. package/lib/typescript/src/timeline/stagger.d.ts.map +1 -0
  191. package/lib/typescript/src/timeline/validation.d.ts +4 -0
  192. package/lib/typescript/src/timeline/validation.d.ts.map +1 -0
  193. package/lib/typescript/src/timeline/wave.d.ts +11 -0
  194. package/lib/typescript/src/timeline/wave.d.ts.map +1 -0
  195. package/lib/typescript/src/types/index.d.ts +6 -0
  196. package/lib/typescript/src/types/index.d.ts.map +1 -0
  197. package/lib/typescript/src/types/recipe.d.ts +123 -0
  198. package/lib/typescript/src/types/recipe.d.ts.map +1 -0
  199. package/lib/typescript/src/types/renderer.d.ts +51 -0
  200. package/lib/typescript/src/types/renderer.d.ts.map +1 -0
  201. package/lib/typescript/src/types/token.d.ts +35 -0
  202. package/lib/typescript/src/types/token.d.ts.map +1 -0
  203. package/package.json +116 -3
  204. package/src/accessibility/index.ts +7 -0
  205. package/src/accessibility/policies.ts +60 -0
  206. package/src/controls/brand.ts +3 -0
  207. package/src/controls/controls.ts +41 -0
  208. package/src/controls/descriptors.ts +83 -0
  209. package/src/controls/index.ts +2 -0
  210. package/src/effects/compose.ts +98 -0
  211. package/src/effects/fade.ts +22 -0
  212. package/src/effects/index.ts +9 -0
  213. package/src/effects/lineReveal.ts +28 -0
  214. package/src/effects/pulse.ts +19 -0
  215. package/src/effects/rise.ts +19 -0
  216. package/src/effects/scale.ts +22 -0
  217. package/src/effects/shake.ts +19 -0
  218. package/src/effects/slide.ts +22 -0
  219. package/src/effects/typewriter.ts +16 -0
  220. package/src/effects/validation.ts +9 -0
  221. package/src/index.tsx +46 -0
  222. package/src/presets/index.ts +42 -0
  223. package/src/recipe/component.tsx +88 -0
  224. package/src/recipe/defineTextMotion.ts +6 -0
  225. package/src/recipe/descriptors.ts +158 -0
  226. package/src/recipe/index.ts +3 -0
  227. package/src/recipe/recipe.ts +359 -0
  228. package/src/renderers/index.ts +2 -0
  229. package/src/renderers/nativeText.tsx +813 -0
  230. package/src/renderers/nativeTextControls.ts +131 -0
  231. package/src/renderers/nativeTextLineLayout.ts +495 -0
  232. package/src/renderers/nativeTextPlayback.ts +204 -0
  233. package/src/renderers/overlayText.tsx +945 -0
  234. package/src/renderers/overlayTextLayout.ts +312 -0
  235. package/src/renderers/rendererMotion.ts +483 -0
  236. package/src/split/createToken.ts +26 -0
  237. package/src/split/custom.ts +92 -0
  238. package/src/split/graphemes.ts +27 -0
  239. package/src/split/index.ts +9 -0
  240. package/src/split/lines.ts +35 -0
  241. package/src/split/segmenter.ts +111 -0
  242. package/src/split/words.ts +28 -0
  243. package/src/timeline/index.ts +4 -0
  244. package/src/timeline/parallel.ts +30 -0
  245. package/src/timeline/sequence.ts +29 -0
  246. package/src/timeline/stagger.ts +60 -0
  247. package/src/timeline/validation.ts +29 -0
  248. package/src/timeline/wave.ts +34 -0
  249. package/src/types/index.ts +34 -0
  250. package/src/types/recipe.ts +195 -0
  251. package/src/types/renderer.ts +91 -0
  252. package/src/types/token.ts +39 -0
@@ -0,0 +1,945 @@
1
+ import {
2
+ useCallback,
3
+ useLayoutEffect,
4
+ useRef,
5
+ useState,
6
+ type ComponentType,
7
+ type MutableRefObject,
8
+ type ReactElement,
9
+ } from 'react';
10
+ import {
11
+ StyleSheet,
12
+ Text,
13
+ View,
14
+ type NativeSyntheticEvent,
15
+ type StyleProp,
16
+ type TextLayoutEvent,
17
+ type TextStyle,
18
+ type ViewStyle,
19
+ } from 'react-native';
20
+ import {
21
+ createAnimatedComponent,
22
+ useAnimatedStyle,
23
+ useReducedMotion,
24
+ useSharedValue,
25
+ type SharedValue,
26
+ } from 'react-native-reanimated';
27
+
28
+ import type { TextMotionControls } from '../controls';
29
+ import type { TextMotionComponentTextProps, TextMotionInternalRecipeConfig } from '../types/recipe';
30
+ import type {
31
+ TextMotionLineMaskCapability,
32
+ TextMotionRenderedLineRenderer,
33
+ TextMotionRendererProps,
34
+ } from '../types/renderer';
35
+
36
+ import {
37
+ createHiddenTokenAccessibilityProps,
38
+ createParentAccessibilityProps,
39
+ parentLabelPolicy,
40
+ resolveAccessibleText,
41
+ } from '../accessibility';
42
+ import { createTextMotionRendererHandle } from '../recipe/descriptors';
43
+ import { createTextMotionRendererCapability } from '../types/renderer';
44
+ import { useNativeTextControlsPlayback } from './nativeTextControls';
45
+ import {
46
+ areNativeTextPlaybackRunsEqual,
47
+ createNativeTextPlaybackRun,
48
+ type NativeTextPlaybackRun,
49
+ } from './nativeTextPlayback';
50
+ import {
51
+ compareOverlayTextLayouts,
52
+ createOverlayTextLayout,
53
+ type OverlayTextLayoutReady,
54
+ type OverlayTextMotionLine,
55
+ } from './overlayTextLayout';
56
+ import {
57
+ clampTextMotionProgress,
58
+ createTextMotionControlledProgressPlan,
59
+ createTextMotionControlledTimelineSpan,
60
+ createTextMotionDelaySecondsByItemIndex,
61
+ createTextMotionItemMotion,
62
+ createOverlayTextStyleTransformStatePair,
63
+ createTextMotionProgressAnimation,
64
+ createTextMotionStyleTransformStatePair,
65
+ readTextMotionProgressForAnimatedStyle,
66
+ shouldRenderTextMotionFinalState,
67
+ type TextMotionControlledProgressPlan,
68
+ type TextMotionItemMotion,
69
+ } from './rendererMotion';
70
+
71
+ type OverlayTextRendererProps = Omit<TextMotionRendererProps, 'recipe'> & {
72
+ recipe: TextMotionInternalRecipeConfig;
73
+ };
74
+
75
+ type OverlayTextContainerProps = Pick<
76
+ TextMotionComponentTextProps,
77
+ | 'accessibilityActions'
78
+ | 'accessibilityHint'
79
+ | 'accessibilityLanguage'
80
+ | 'accessibilityRole'
81
+ | 'accessibilityState'
82
+ | 'accessibilityValue'
83
+ | 'nativeID'
84
+ | 'onAccessibilityAction'
85
+ | 'testID'
86
+ > &
87
+ ReturnType<typeof createParentAccessibilityProps>;
88
+
89
+ type OverlayTextTypographyProps = Pick<
90
+ TextMotionComponentTextProps,
91
+ 'allowFontScaling' | 'maxFontSizeMultiplier'
92
+ >;
93
+
94
+ type OverlayTextFinalSourceProps = {
95
+ children: OverlayTextRendererProps['children'];
96
+ containerProps: OverlayTextContainerProps;
97
+ sourceAccessibilityProps: ReturnType<typeof createHiddenTokenAccessibilityProps>;
98
+ textStyle: StyleProp<TextStyle>;
99
+ typographyProps: OverlayTextTypographyProps;
100
+ };
101
+
102
+ type OverlayTextActiveRendererProps = {
103
+ children: OverlayTextRendererProps['children'];
104
+ containerProps: OverlayTextContainerProps;
105
+ recipe: OverlayTextRendererProps['recipe'];
106
+ sourceAccessibilityProps: ReturnType<typeof createHiddenTokenAccessibilityProps>;
107
+ text: string;
108
+ textProps: OverlayTextRendererProps['textProps'];
109
+ textStyle: StyleProp<TextStyle>;
110
+ typographyProps: OverlayTextTypographyProps;
111
+ };
112
+
113
+ type OverlayTextAnimatedStyleOptions =
114
+ | {
115
+ controlled: true;
116
+ controlledProgressPlan: TextMotionControlledProgressPlan;
117
+ progress: SharedValue<number>;
118
+ renderFinalState: boolean;
119
+ tokenMotion: TextMotionItemMotion;
120
+ }
121
+ | {
122
+ controlled: false;
123
+ progress: SharedValue<number>;
124
+ renderFinalState: boolean;
125
+ tokenMotion: TextMotionItemMotion;
126
+ };
127
+
128
+ type OverlayTextAnimatedLineProps = {
129
+ children: string;
130
+ line: OverlayTextMotionLine;
131
+ maskTestID?: string;
132
+ motionTestID?: string;
133
+ paragraphTestID?: string;
134
+ revealContentMotion: TextMotionItemMotion;
135
+ textStyle: StyleProp<TextStyle>;
136
+ frameMotion: TextMotionItemMotion;
137
+ typographyProps: OverlayTextTypographyProps;
138
+ };
139
+
140
+ type OverlayTextUncontrolledLineProps = OverlayTextAnimatedLineProps & {
141
+ controls?: TextMotionControls;
142
+ playbackIdentity: string;
143
+ };
144
+
145
+ type OverlayTextControlledLineProps = OverlayTextAnimatedLineProps & {
146
+ controlledProgress: SharedValue<number>;
147
+ controlledProgressPlan: TextMotionControlledProgressPlan;
148
+ };
149
+
150
+ type OverlayTextLineMaskProps = Pick<
151
+ OverlayTextAnimatedLineProps,
152
+ | 'children'
153
+ | 'line'
154
+ | 'maskTestID'
155
+ | 'motionTestID'
156
+ | 'paragraphTestID'
157
+ | 'textStyle'
158
+ | 'typographyProps'
159
+ > & {
160
+ frameAnimatedStyle: ReturnType<typeof useOverlayTextAnimatedStyle>;
161
+ revealContentAnimatedStyle: ReturnType<typeof useOverlayTextAnimatedStyle>;
162
+ };
163
+
164
+ type OverlayTextLayoutResult = {
165
+ fallbackReason?: string;
166
+ inputKey: string;
167
+ layout?: OverlayTextLayoutReady;
168
+ };
169
+
170
+ export type OverlayTextRendererOptions = {
171
+ /** Prefix used for generated line mask `testID` values. */
172
+ testIDPrefix?: string;
173
+ };
174
+
175
+ const AnimatedView = createAnimatedComponent(View);
176
+ const LINE_MASK_CAPABILITY: TextMotionLineMaskCapability =
177
+ createTextMotionRendererCapability('line-mask');
178
+ const HIDDEN_ACCESSIBILITY_PROPS = {
179
+ accessibilityElementsHidden: true,
180
+ accessible: false,
181
+ importantForAccessibility: 'no-hide-descendants',
182
+ } as const;
183
+ const OVERLAY_CONTAINER_STYLE = {
184
+ ...StyleSheet.absoluteFill,
185
+ pointerEvents: 'none',
186
+ } as const satisfies ViewStyle;
187
+ const PENDING_OVERLAY_CONTAINER_STYLE = {
188
+ opacity: 0,
189
+ } as const satisfies ViewStyle;
190
+ const SOURCE_LAYOUT_STYLE = {
191
+ alignSelf: 'stretch',
192
+ } as const satisfies TextStyle;
193
+ const READY_SOURCE_STYLE = {
194
+ opacity: 0,
195
+ } as const satisfies TextStyle;
196
+ const MASK_STYLE = {
197
+ left: 0,
198
+ overflow: 'hidden',
199
+ position: 'absolute',
200
+ right: 0,
201
+ } as const satisfies ViewStyle;
202
+ const LINE_ANIMATION_STYLE = {
203
+ left: 0,
204
+ position: 'absolute',
205
+ top: 0,
206
+ width: '100%',
207
+ } as const satisfies ViewStyle;
208
+ const LINE_CONTENT_CLIP_STYLE = {
209
+ overflow: 'hidden',
210
+ width: '100%',
211
+ } as const satisfies ViewStyle;
212
+ const PARAGRAPH_COPY_STYLE = {
213
+ left: 0,
214
+ position: 'absolute',
215
+ width: '100%',
216
+ } as const satisfies TextStyle;
217
+ const OVERLAY_TEXT_SHAPING_STYLE_KEYS = [
218
+ 'direction',
219
+ 'fontFamily',
220
+ 'fontSize',
221
+ 'fontStyle',
222
+ 'fontVariant',
223
+ 'fontWeight',
224
+ 'includeFontPadding',
225
+ 'letterSpacing',
226
+ 'lineHeight',
227
+ 'textAlign',
228
+ 'textAlignVertical',
229
+ 'textTransform',
230
+ 'verticalAlign',
231
+ 'writingDirection',
232
+ ] as const satisfies readonly (keyof TextStyle)[];
233
+ const OVERLAY_TEXT_BOX_LAYOUT_STYLE_KEYS = [
234
+ 'alignSelf',
235
+ 'aspectRatio',
236
+ 'borderBottomWidth',
237
+ 'borderEndWidth',
238
+ 'borderLeftWidth',
239
+ 'borderRightWidth',
240
+ 'borderStartWidth',
241
+ 'borderTopWidth',
242
+ 'borderWidth',
243
+ 'bottom',
244
+ 'boxSizing',
245
+ 'display',
246
+ 'end',
247
+ 'flex',
248
+ 'flexBasis',
249
+ 'flexGrow',
250
+ 'flexShrink',
251
+ 'height',
252
+ 'inset',
253
+ 'insetBlock',
254
+ 'insetBlockEnd',
255
+ 'insetBlockStart',
256
+ 'insetInline',
257
+ 'insetInlineEnd',
258
+ 'insetInlineStart',
259
+ 'left',
260
+ 'margin',
261
+ 'marginBlock',
262
+ 'marginBlockEnd',
263
+ 'marginBlockStart',
264
+ 'marginBottom',
265
+ 'marginEnd',
266
+ 'marginHorizontal',
267
+ 'marginInline',
268
+ 'marginInlineEnd',
269
+ 'marginInlineStart',
270
+ 'marginLeft',
271
+ 'marginRight',
272
+ 'marginStart',
273
+ 'marginTop',
274
+ 'marginVertical',
275
+ 'maxHeight',
276
+ 'maxWidth',
277
+ 'minHeight',
278
+ 'minWidth',
279
+ 'padding',
280
+ 'paddingBlock',
281
+ 'paddingBlockEnd',
282
+ 'paddingBlockStart',
283
+ 'paddingBottom',
284
+ 'paddingEnd',
285
+ 'paddingHorizontal',
286
+ 'paddingInline',
287
+ 'paddingInlineEnd',
288
+ 'paddingInlineStart',
289
+ 'paddingLeft',
290
+ 'paddingRight',
291
+ 'paddingStart',
292
+ 'paddingTop',
293
+ 'paddingVertical',
294
+ 'position',
295
+ 'right',
296
+ 'start',
297
+ 'top',
298
+ 'width',
299
+ ] as const satisfies readonly (keyof TextStyle)[];
300
+ const OVERLAY_TEXT_LAYOUT_STYLE_KEYS = [
301
+ ...OVERLAY_TEXT_SHAPING_STYLE_KEYS,
302
+ ...OVERLAY_TEXT_BOX_LAYOUT_STYLE_KEYS,
303
+ ] as const;
304
+
305
+ type OverlayTextLayoutStyleSnapshot = Partial<
306
+ Record<(typeof OVERLAY_TEXT_LAYOUT_STYLE_KEYS)[number], unknown>
307
+ >;
308
+
309
+ function createContainerProps(
310
+ textProps: TextMotionComponentTextProps | undefined,
311
+ parentAccessibilityProps: ReturnType<typeof createParentAccessibilityProps>,
312
+ ): OverlayTextContainerProps {
313
+ return {
314
+ accessibilityActions: textProps?.accessibilityActions,
315
+ accessibilityHint: textProps?.accessibilityHint,
316
+ accessibilityLabel:
317
+ textProps?.accessibilityLabel ?? parentAccessibilityProps.accessibilityLabel,
318
+ accessibilityLanguage: textProps?.accessibilityLanguage,
319
+ accessibilityRole: textProps?.accessibilityRole,
320
+ accessibilityState: textProps?.accessibilityState,
321
+ accessibilityValue: textProps?.accessibilityValue,
322
+ accessible: parentAccessibilityProps.accessible,
323
+ nativeID: textProps?.nativeID,
324
+ onAccessibilityAction: textProps?.onAccessibilityAction,
325
+ testID: textProps?.testID,
326
+ };
327
+ }
328
+
329
+ function createTypographyProps(
330
+ textProps: TextMotionComponentTextProps | undefined,
331
+ ): OverlayTextTypographyProps {
332
+ return {
333
+ allowFontScaling: textProps?.allowFontScaling,
334
+ maxFontSizeMultiplier: textProps?.maxFontSizeMultiplier,
335
+ };
336
+ }
337
+
338
+ function OverlayTextFinalSource({
339
+ children,
340
+ containerProps,
341
+ sourceAccessibilityProps,
342
+ textStyle,
343
+ typographyProps,
344
+ }: OverlayTextFinalSourceProps) {
345
+ return (
346
+ <View {...containerProps}>
347
+ <Text
348
+ {...sourceAccessibilityProps}
349
+ {...typographyProps}
350
+ style={[textStyle, SOURCE_LAYOUT_STYLE]}
351
+ >
352
+ {children}
353
+ </Text>
354
+ </View>
355
+ );
356
+ }
357
+
358
+ function createInitialTextStyle(tokenMotion: TextMotionItemMotion): StyleProp<TextStyle> {
359
+ return {
360
+ opacity: tokenMotion.initial.opacity,
361
+ transform: [
362
+ { translateX: tokenMotion.initial.translateX },
363
+ { translateY: tokenMotion.initial.translateY },
364
+ { scale: tokenMotion.initial.scale },
365
+ ],
366
+ };
367
+ }
368
+
369
+ function createSourceTextMotionStyle(
370
+ layout: OverlayTextLayoutReady | undefined,
371
+ pendingMotion: TextMotionItemMotion,
372
+ ): StyleProp<TextStyle> {
373
+ if (layout) {
374
+ return READY_SOURCE_STYLE;
375
+ }
376
+
377
+ if (pendingMotion.initial.scale !== 1) {
378
+ return PENDING_OVERLAY_CONTAINER_STYLE;
379
+ }
380
+
381
+ return createInitialTextStyle(pendingMotion);
382
+ }
383
+
384
+ function createLineFrameStyle(line: OverlayTextMotionLine): StyleProp<ViewStyle> {
385
+ return {
386
+ height: line.height,
387
+ top: line.y,
388
+ transformOrigin: [line.x + line.width / 2, line.height / 2, 0],
389
+ };
390
+ }
391
+
392
+ function isSerializableRecord(value: unknown): value is Record<string, unknown> {
393
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
394
+ }
395
+
396
+ function createStableLayoutInputValue(value: unknown): unknown {
397
+ if (Array.isArray(value)) {
398
+ return value.map(createStableLayoutInputValue);
399
+ }
400
+
401
+ if (!isSerializableRecord(value)) {
402
+ return value;
403
+ }
404
+
405
+ const entries = Object.entries(value);
406
+
407
+ entries.sort(([left], [right]) => left.localeCompare(right));
408
+
409
+ return Object.fromEntries(
410
+ entries.map(([key, nestedValue]) => [key, createStableLayoutInputValue(nestedValue)]),
411
+ );
412
+ }
413
+
414
+ function createOverlayTextLayoutStyleSnapshot(
415
+ style: StyleProp<TextStyle>,
416
+ ): OverlayTextLayoutStyleSnapshot {
417
+ const flattenedStyle = StyleSheet.flatten(style) ?? {};
418
+
419
+ return OVERLAY_TEXT_LAYOUT_STYLE_KEYS.reduce<OverlayTextLayoutStyleSnapshot>((snapshot, key) => {
420
+ const value = flattenedStyle[key];
421
+
422
+ if (value === undefined) {
423
+ return snapshot;
424
+ }
425
+
426
+ return {
427
+ ...snapshot,
428
+ [key]: createStableLayoutInputValue(value),
429
+ };
430
+ }, {});
431
+ }
432
+
433
+ function createOverlayTextInputKey(
434
+ text: string,
435
+ textProps: TextMotionComponentTextProps | undefined,
436
+ ): string {
437
+ return JSON.stringify({
438
+ allowFontScaling: textProps?.allowFontScaling,
439
+ maxFontSizeMultiplier: textProps?.maxFontSizeMultiplier,
440
+ style: createOverlayTextLayoutStyleSnapshot(textProps?.style),
441
+ text,
442
+ });
443
+ }
444
+
445
+ function createParagraphCopyOffset(line: OverlayTextMotionLine): number {
446
+ return line.y === 0 ? 0 : -line.y;
447
+ }
448
+
449
+ function OverlayTextLineMask({
450
+ children,
451
+ frameAnimatedStyle,
452
+ line,
453
+ maskTestID,
454
+ motionTestID,
455
+ paragraphTestID,
456
+ revealContentAnimatedStyle,
457
+ textStyle,
458
+ typographyProps,
459
+ }: OverlayTextLineMaskProps): ReactElement {
460
+ return (
461
+ <AnimatedView
462
+ {...HIDDEN_ACCESSIBILITY_PROPS}
463
+ pointerEvents="none"
464
+ style={[MASK_STYLE, createLineFrameStyle(line), frameAnimatedStyle]}
465
+ testID={maskTestID}
466
+ >
467
+ <AnimatedView
468
+ style={[LINE_ANIMATION_STYLE, revealContentAnimatedStyle] as StyleProp<ViewStyle>}
469
+ testID={motionTestID}
470
+ >
471
+ {/* Isolate this source line before moving it through the reveal mask. */}
472
+ <View collapsable={false} style={[LINE_CONTENT_CLIP_STYLE, { height: line.height }]}>
473
+ <Text
474
+ {...HIDDEN_ACCESSIBILITY_PROPS}
475
+ {...typographyProps}
476
+ style={[textStyle, PARAGRAPH_COPY_STYLE, { top: createParagraphCopyOffset(line) }]}
477
+ testID={paragraphTestID}
478
+ >
479
+ {children}
480
+ </Text>
481
+ </View>
482
+ </AnimatedView>
483
+ </AnimatedView>
484
+ );
485
+ }
486
+
487
+ function useOverlayTextAnimatedStyle(options: OverlayTextAnimatedStyleOptions) {
488
+ const { controlled, progress, renderFinalState, tokenMotion } = options;
489
+ const initialOpacity = tokenMotion.initial.opacity;
490
+ const initialScale = tokenMotion.initial.scale;
491
+ const initialTranslateX = tokenMotion.initial.translateX;
492
+ const initialTranslateY = tokenMotion.initial.translateY;
493
+ const targetOpacity = tokenMotion.target.opacity;
494
+ const targetScale = tokenMotion.target.scale;
495
+ const targetTranslateX = tokenMotion.target.translateX;
496
+ const targetTranslateY = tokenMotion.target.translateY;
497
+ const pulseScale = tokenMotion.pulseScale;
498
+ const itemDelaySeconds = controlled ? options.controlledProgressPlan.itemDelaySeconds : 0;
499
+ const totalTimelineSpan = controlled ? options.controlledProgressPlan.totalTimelineSpan : 1;
500
+
501
+ return useAnimatedStyle(() => {
502
+ const current = readTextMotionProgressForAnimatedStyle({
503
+ controlledProgressPlan: controlled
504
+ ? {
505
+ itemDelaySeconds,
506
+ totalTimelineSpan,
507
+ }
508
+ : undefined,
509
+ progress,
510
+ renderFinalState,
511
+ });
512
+ const clampedCurrent = clampTextMotionProgress(current);
513
+ const baseScale = initialScale + (targetScale - initialScale) * current;
514
+ const pulseProgress = 1 - Math.abs(clampedCurrent * 2 - 1);
515
+ const pulseMultiplier = 1 + (pulseScale - 1) * pulseProgress;
516
+
517
+ return {
518
+ opacity: initialOpacity + (targetOpacity - initialOpacity) * current,
519
+ transform: [
520
+ {
521
+ translateX: initialTranslateX + (targetTranslateX - initialTranslateX) * current,
522
+ },
523
+ {
524
+ translateY: initialTranslateY + (targetTranslateY - initialTranslateY) * current,
525
+ },
526
+ {
527
+ scale: baseScale * pulseMultiplier,
528
+ },
529
+ ],
530
+ };
531
+ }, [
532
+ initialOpacity,
533
+ initialScale,
534
+ initialTranslateX,
535
+ initialTranslateY,
536
+ controlled,
537
+ progress,
538
+ pulseScale,
539
+ renderFinalState,
540
+ targetOpacity,
541
+ targetScale,
542
+ targetTranslateX,
543
+ targetTranslateY,
544
+ itemDelaySeconds,
545
+ totalTimelineSpan,
546
+ ]);
547
+ }
548
+
549
+ function OverlayTextUncontrolledLine({
550
+ children,
551
+ controls,
552
+ line,
553
+ maskTestID,
554
+ motionTestID,
555
+ paragraphTestID,
556
+ playbackIdentity,
557
+ revealContentMotion,
558
+ textStyle,
559
+ frameMotion,
560
+ typographyProps,
561
+ }: OverlayTextUncontrolledLineProps) {
562
+ const reducedMotionEnabled = useReducedMotion();
563
+ const renderFinalState = shouldRenderTextMotionFinalState(
564
+ reducedMotionEnabled,
565
+ frameMotion.reducedMotion,
566
+ );
567
+ const progress = useSharedValue(renderFinalState ? 1 : 0);
568
+ const playbackRun = createNativeTextPlaybackRun(frameMotion, renderFinalState, playbackIdentity);
569
+ const previousPlaybackRun = useRef<NativeTextPlaybackRun | undefined>(undefined);
570
+ const frameAnimatedStyle = useOverlayTextAnimatedStyle({
571
+ controlled: false,
572
+ progress,
573
+ renderFinalState,
574
+ tokenMotion: frameMotion,
575
+ });
576
+ const revealContentAnimatedStyle = useOverlayTextAnimatedStyle({
577
+ controlled: false,
578
+ progress,
579
+ renderFinalState,
580
+ tokenMotion: revealContentMotion,
581
+ });
582
+
583
+ useLayoutEffect(() => {
584
+ if (areNativeTextPlaybackRunsEqual(previousPlaybackRun.current, playbackRun)) {
585
+ return;
586
+ }
587
+
588
+ previousPlaybackRun.current = playbackRun;
589
+
590
+ if (playbackRun.renderFinalState) {
591
+ progress.value = 1;
592
+ return;
593
+ }
594
+
595
+ progress.value = 0;
596
+ progress.value = createTextMotionProgressAnimation(playbackRun);
597
+ });
598
+
599
+ useNativeTextControlsPlayback({
600
+ controls,
601
+ createProgressAnimation: createTextMotionProgressAnimation,
602
+ progress,
603
+ renderFinalState,
604
+ tokenMotion: frameMotion,
605
+ });
606
+
607
+ return (
608
+ <OverlayTextLineMask
609
+ frameAnimatedStyle={frameAnimatedStyle}
610
+ line={line}
611
+ maskTestID={maskTestID}
612
+ motionTestID={motionTestID}
613
+ paragraphTestID={paragraphTestID}
614
+ revealContentAnimatedStyle={revealContentAnimatedStyle}
615
+ textStyle={textStyle}
616
+ typographyProps={typographyProps}
617
+ >
618
+ {children}
619
+ </OverlayTextLineMask>
620
+ );
621
+ }
622
+
623
+ function OverlayTextControlledLine({
624
+ children,
625
+ controlledProgress,
626
+ controlledProgressPlan,
627
+ line,
628
+ maskTestID,
629
+ motionTestID,
630
+ paragraphTestID,
631
+ revealContentMotion,
632
+ textStyle,
633
+ frameMotion,
634
+ typographyProps,
635
+ }: OverlayTextControlledLineProps) {
636
+ const reducedMotionEnabled = useReducedMotion();
637
+ const renderFinalState = shouldRenderTextMotionFinalState(
638
+ reducedMotionEnabled,
639
+ frameMotion.reducedMotion,
640
+ );
641
+ const frameAnimatedStyle = useOverlayTextAnimatedStyle({
642
+ controlled: true,
643
+ controlledProgressPlan,
644
+ progress: controlledProgress,
645
+ renderFinalState,
646
+ tokenMotion: frameMotion,
647
+ });
648
+ const revealContentAnimatedStyle = useOverlayTextAnimatedStyle({
649
+ controlled: true,
650
+ controlledProgressPlan,
651
+ progress: controlledProgress,
652
+ renderFinalState,
653
+ tokenMotion: revealContentMotion,
654
+ });
655
+
656
+ return (
657
+ <OverlayTextLineMask
658
+ frameAnimatedStyle={frameAnimatedStyle}
659
+ line={line}
660
+ maskTestID={maskTestID}
661
+ motionTestID={motionTestID}
662
+ paragraphTestID={paragraphTestID}
663
+ revealContentAnimatedStyle={revealContentAnimatedStyle}
664
+ textStyle={textStyle}
665
+ typographyProps={typographyProps}
666
+ >
667
+ {children}
668
+ </OverlayTextLineMask>
669
+ );
670
+ }
671
+
672
+ function warnOverlayTextFallbackOnce(warnedRef: MutableRefObject<boolean>, reason: string) {
673
+ if (warnedRef.current) {
674
+ return;
675
+ }
676
+
677
+ warnedRef.current = true;
678
+
679
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
680
+ console.warn(
681
+ `@react-native-motion-kit/text-motion overlayText() received unsupported line layout (${reason}); rendering readable final text instead.`,
682
+ );
683
+ }
684
+ }
685
+
686
+ function isTextLayoutEventRecord(value: unknown): value is Record<string, unknown> {
687
+ return typeof value === 'object' && value !== null;
688
+ }
689
+
690
+ function readTextLayoutNativeEvent(event: unknown): unknown {
691
+ if (!isTextLayoutEventRecord(event) || !('nativeEvent' in event)) {
692
+ return event;
693
+ }
694
+
695
+ const nativeEvent = event.nativeEvent;
696
+
697
+ if (!isTextLayoutEventRecord(nativeEvent) || 'lines' in nativeEvent) {
698
+ return nativeEvent;
699
+ }
700
+
701
+ return 'nativeEvent' in nativeEvent ? nativeEvent.nativeEvent : nativeEvent;
702
+ }
703
+
704
+ function createMaskTestID(prefix: string | undefined, line: OverlayTextMotionLine) {
705
+ return prefix ? `${prefix}-${line.motionIndex}` : undefined;
706
+ }
707
+
708
+ function createParagraphTestID(prefix: string | undefined, line: OverlayTextMotionLine) {
709
+ return prefix ? `${prefix}-${line.motionIndex}-copy` : undefined;
710
+ }
711
+
712
+ function createMotionTestID(prefix: string | undefined, line: OverlayTextMotionLine) {
713
+ return prefix ? `${prefix}-${line.motionIndex}-motion` : undefined;
714
+ }
715
+
716
+ function createOverlayTestID(prefix: string | undefined) {
717
+ return prefix ? `${prefix}-overlay` : undefined;
718
+ }
719
+
720
+ function createOverlayTextRendererComponent(
721
+ options: OverlayTextRendererOptions,
722
+ ): ComponentType<OverlayTextRendererProps> {
723
+ function OverlayTextActiveRenderer({
724
+ children,
725
+ containerProps,
726
+ recipe,
727
+ sourceAccessibilityProps,
728
+ text,
729
+ textProps,
730
+ textStyle,
731
+ typographyProps,
732
+ }: OverlayTextActiveRendererProps) {
733
+ const inputKey = createOverlayTextInputKey(text, textProps);
734
+ const styleState = createTextMotionStyleTransformStatePair(recipe.effects, 'overlayText()');
735
+ const overlayStyleState = createOverlayTextStyleTransformStatePair(recipe.effects);
736
+ const delaySecondsByLineIndex = createTextMotionDelaySecondsByItemIndex(recipe, 1);
737
+ const pendingMotion = createTextMotionItemMotion(
738
+ recipe,
739
+ styleState,
740
+ delaySecondsByLineIndex[0] ?? 0,
741
+ );
742
+ const [layoutResult, setLayoutResult] = useState<OverlayTextLayoutResult | undefined>(
743
+ undefined,
744
+ );
745
+ const boundInputKey = layoutResult?.inputKey;
746
+ const retainedLayout = layoutResult?.layout;
747
+ const currentLayout = boundInputKey === inputKey ? retainedLayout : undefined;
748
+ const fallbackReason = boundInputKey === inputKey ? layoutResult?.fallbackReason : undefined;
749
+ const layoutPending = Boolean(retainedLayout && boundInputKey !== inputKey);
750
+ const warnedFallback = useRef(false);
751
+ const externalProgress = textProps?.progress;
752
+ const controls = textProps?.controls;
753
+ const delaySecondsByMotionIndex = retainedLayout
754
+ ? createTextMotionDelaySecondsByItemIndex(recipe, retainedLayout.motionCount)
755
+ : [];
756
+ const totalTimelineSpan = createTextMotionControlledTimelineSpan(delaySecondsByMotionIndex);
757
+ const onTextLayout = useCallback(
758
+ (event: NativeSyntheticEvent<TextLayoutEvent['nativeEvent']>) => {
759
+ const nextLayout = createOverlayTextLayout(text, readTextLayoutNativeEvent(event));
760
+
761
+ if (nextLayout.kind === 'static') {
762
+ setLayoutResult({ inputKey });
763
+ return;
764
+ }
765
+
766
+ if (nextLayout.kind === 'fallback') {
767
+ setLayoutResult({ fallbackReason: nextLayout.reason, inputKey });
768
+ warnOverlayTextFallbackOnce(warnedFallback, nextLayout.reason);
769
+ return;
770
+ }
771
+
772
+ const change = compareOverlayTextLayouts(retainedLayout, nextLayout);
773
+
774
+ if (change === 'identical' && boundInputKey === inputKey) {
775
+ return;
776
+ }
777
+
778
+ setLayoutResult({ inputKey, layout: nextLayout });
779
+ },
780
+ [boundInputKey, inputKey, retainedLayout, text],
781
+ );
782
+
783
+ if (text.trim().length === 0 || fallbackReason) {
784
+ return (
785
+ <OverlayTextFinalSource
786
+ containerProps={containerProps}
787
+ sourceAccessibilityProps={sourceAccessibilityProps}
788
+ textStyle={textStyle}
789
+ typographyProps={typographyProps}
790
+ >
791
+ {children}
792
+ </OverlayTextFinalSource>
793
+ );
794
+ }
795
+
796
+ return (
797
+ <View {...containerProps} style={{ position: 'relative' }}>
798
+ <Text
799
+ {...sourceAccessibilityProps}
800
+ {...typographyProps}
801
+ onTextLayout={onTextLayout}
802
+ style={[
803
+ textStyle,
804
+ SOURCE_LAYOUT_STYLE,
805
+ createSourceTextMotionStyle(currentLayout, pendingMotion),
806
+ ]}
807
+ >
808
+ {children}
809
+ </Text>
810
+ {retainedLayout ? (
811
+ <View
812
+ {...HIDDEN_ACCESSIBILITY_PROPS}
813
+ pointerEvents="none"
814
+ style={[
815
+ OVERLAY_CONTAINER_STYLE,
816
+ layoutPending ? PENDING_OVERLAY_CONTAINER_STYLE : undefined,
817
+ ]}
818
+ testID={createOverlayTestID(options.testIDPrefix)}
819
+ >
820
+ {retainedLayout.motionLines.map((line) => {
821
+ const frameMotion = createTextMotionItemMotion(
822
+ recipe,
823
+ overlayStyleState.frame,
824
+ delaySecondsByMotionIndex[line.motionIndex] ?? 0,
825
+ );
826
+ const revealContentMotion = createTextMotionItemMotion(
827
+ recipe,
828
+ overlayStyleState.revealContent,
829
+ delaySecondsByMotionIndex[line.motionIndex] ?? 0,
830
+ );
831
+
832
+ if (!externalProgress) {
833
+ return (
834
+ <OverlayTextUncontrolledLine
835
+ controls={controls}
836
+ key={line.index}
837
+ line={line}
838
+ maskTestID={createMaskTestID(options.testIDPrefix, line)}
839
+ motionTestID={createMotionTestID(options.testIDPrefix, line)}
840
+ paragraphTestID={createParagraphTestID(options.testIDPrefix, line)}
841
+ playbackIdentity={retainedLayout.playbackSignature}
842
+ revealContentMotion={revealContentMotion}
843
+ textStyle={textStyle}
844
+ frameMotion={frameMotion}
845
+ typographyProps={typographyProps}
846
+ >
847
+ {text}
848
+ </OverlayTextUncontrolledLine>
849
+ );
850
+ }
851
+
852
+ return (
853
+ <OverlayTextControlledLine
854
+ controlledProgress={externalProgress}
855
+ controlledProgressPlan={createTextMotionControlledProgressPlan(
856
+ line.motionIndex,
857
+ delaySecondsByMotionIndex,
858
+ totalTimelineSpan,
859
+ )}
860
+ key={line.index}
861
+ line={line}
862
+ maskTestID={createMaskTestID(options.testIDPrefix, line)}
863
+ motionTestID={createMotionTestID(options.testIDPrefix, line)}
864
+ paragraphTestID={createParagraphTestID(options.testIDPrefix, line)}
865
+ revealContentMotion={revealContentMotion}
866
+ textStyle={textStyle}
867
+ frameMotion={frameMotion}
868
+ typographyProps={typographyProps}
869
+ >
870
+ {text}
871
+ </OverlayTextControlledLine>
872
+ );
873
+ })}
874
+ </View>
875
+ ) : null}
876
+ </View>
877
+ );
878
+ }
879
+
880
+ function OverlayTextRenderer({ children, recipe, textProps, tokens }: OverlayTextRendererProps) {
881
+ const reducedMotionEnabled = useReducedMotion();
882
+ const text = typeof children === 'string' ? children : '';
883
+ const textStyle = textProps?.style;
884
+ const policy = recipe.accessibility ?? parentLabelPolicy();
885
+ const accessibilityLabel = resolveAccessibleText(text, tokens);
886
+ const parentAccessibilityProps = createParentAccessibilityProps(policy, accessibilityLabel);
887
+ const sourceAccessibilityProps = createHiddenTokenAccessibilityProps(policy);
888
+ const containerProps = createContainerProps(textProps, parentAccessibilityProps);
889
+ const typographyProps = createTypographyProps(textProps);
890
+ const externalProgress = textProps?.progress;
891
+ const controls = textProps?.controls;
892
+
893
+ if (externalProgress && controls) {
894
+ throw new Error(
895
+ '@react-native-motion-kit/text-motion cannot receive both progress and controls. Use progress for raw app-owned values, or controls for event-driven playback.',
896
+ );
897
+ }
898
+
899
+ if (reducedMotionEnabled) {
900
+ return (
901
+ <OverlayTextFinalSource
902
+ containerProps={containerProps}
903
+ sourceAccessibilityProps={sourceAccessibilityProps}
904
+ textStyle={textStyle}
905
+ typographyProps={typographyProps}
906
+ >
907
+ {children}
908
+ </OverlayTextFinalSource>
909
+ );
910
+ }
911
+
912
+ return (
913
+ <OverlayTextActiveRenderer
914
+ containerProps={containerProps}
915
+ recipe={recipe}
916
+ sourceAccessibilityProps={sourceAccessibilityProps}
917
+ text={text}
918
+ textProps={textProps}
919
+ textStyle={textStyle}
920
+ typographyProps={typographyProps}
921
+ >
922
+ {children}
923
+ </OverlayTextActiveRenderer>
924
+ );
925
+ }
926
+
927
+ OverlayTextRenderer.displayName = 'OverlayTextRenderer';
928
+
929
+ return OverlayTextRenderer;
930
+ }
931
+
932
+ /** Render one native paragraph with animated masks for each actual rendered line. */
933
+ export function overlayText(
934
+ options: OverlayTextRendererOptions = {},
935
+ ): TextMotionRenderedLineRenderer<
936
+ TextMotionLineMaskCapability | 'style-transform',
937
+ OverlayTextRendererProps['recipe']
938
+ > {
939
+ return createTextMotionRendererHandle({
940
+ kind: 'overlayText',
941
+ capabilities: [LINE_MASK_CAPABILITY, 'style-transform'],
942
+ motionUnit: 'rendered-line',
943
+ Component: createOverlayTextRendererComponent(options),
944
+ });
945
+ }