@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,813 @@
1
+ import { useLayoutEffect, useRef, type ComponentType, type ReactNode } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type TextProps,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+ import {
12
+ createAnimatedComponent,
13
+ useAnimatedStyle,
14
+ useReducedMotion,
15
+ useSharedValue,
16
+ type SharedValue,
17
+ } from 'react-native-reanimated';
18
+
19
+ import type { TextMotionControls } from '../controls';
20
+ import type { TextMotionComponentTextProps, TextMotionInternalRecipeConfig } from '../types/recipe';
21
+ import type { TextMotionRendererProps, TextMotionSourceTokenRenderer } from '../types/renderer';
22
+ import type { TextMotionToken } from '../types/token';
23
+
24
+ import {
25
+ createHiddenTokenAccessibilityProps,
26
+ createParentAccessibilityProps,
27
+ parentLabelPolicy,
28
+ resolveAccessibleText,
29
+ } from '../accessibility';
30
+ import { createTextMotionRendererHandle } from '../recipe/descriptors';
31
+ import { useNativeTextControlsPlayback } from './nativeTextControls';
32
+ import {
33
+ areNativeTextPlaybackRunsEqual,
34
+ createNativeTextPlaybackRun,
35
+ type NativeTextPlaybackRun,
36
+ } from './nativeTextPlayback';
37
+ import {
38
+ clampTextMotionProgress,
39
+ createTextMotionControlledProgressPlan,
40
+ createTextMotionControlledTimelineSpan,
41
+ createTextMotionDelaySecondsByItemIndex,
42
+ createTextMotionItemMotion,
43
+ createTextMotionProgressAnimation,
44
+ createTextMotionStyleTransformStatePair,
45
+ readTextMotionProgressForAnimatedStyle,
46
+ shouldRenderTextMotionFinalState,
47
+ type TextMotionControlledProgressPlan,
48
+ type TextMotionItemMotion,
49
+ type TextMotionStyleTransformStatePair,
50
+ } from './rendererMotion';
51
+
52
+ type AnimatedTextProps = TextProps & {
53
+ children?: ReactNode;
54
+ };
55
+
56
+ type StaticNativeTextTokenProps = AnimatedTextProps & {
57
+ tokenMotion?: undefined;
58
+ };
59
+
60
+ type ControlledNativeTextTokenProps = AnimatedTextProps & {
61
+ controlledProgress: SharedValue<number>;
62
+ controlledProgressPlan: TextMotionControlledProgressPlan;
63
+ tokenMotion: TextMotionItemMotion;
64
+ };
65
+
66
+ type UncontrolledNativeTextTokenProps = AnimatedTextProps & {
67
+ controls?: TextMotionControls;
68
+ playbackText: string;
69
+ tokenMotion: TextMotionItemMotion;
70
+ };
71
+
72
+ type NativeTextTokenProps =
73
+ | StaticNativeTextTokenProps
74
+ | ControlledNativeTextTokenProps
75
+ | UncontrolledNativeTextTokenProps;
76
+
77
+ type NativeTextAnimatedStyleOptions =
78
+ | {
79
+ controlled: true;
80
+ controlledProgressPlan: TextMotionControlledProgressPlan;
81
+ progress: SharedValue<number>;
82
+ renderFinalState: boolean;
83
+ tokenMotion: TextMotionItemMotion;
84
+ }
85
+ | {
86
+ controlled: false;
87
+ progress: SharedValue<number>;
88
+ renderFinalState: boolean;
89
+ tokenMotion: TextMotionItemMotion;
90
+ };
91
+
92
+ type ControlledAnimatedNativeTextTokenProps = AnimatedTextProps & {
93
+ controlledProgress: SharedValue<number>;
94
+ controlledProgressPlan: TextMotionControlledProgressPlan;
95
+ tokenMotion: TextMotionItemMotion;
96
+ };
97
+
98
+ type UncontrolledAnimatedNativeTextTokenProps = AnimatedTextProps & {
99
+ controls?: TextMotionControls;
100
+ playbackText: string;
101
+ tokenMotion: TextMotionItemMotion;
102
+ };
103
+
104
+ type NativeTextAnimatedTokenContainerProps = Pick<
105
+ AnimatedTextProps,
106
+ 'accessibilityElementsHidden' | 'accessible' | 'importantForAccessibility' | 'testID'
107
+ >;
108
+
109
+ type NativeTextContainerProps = Pick<
110
+ TextMotionComponentTextProps,
111
+ | 'accessibilityActions'
112
+ | 'accessibilityHint'
113
+ | 'accessibilityLanguage'
114
+ | 'accessibilityRole'
115
+ | 'accessibilityState'
116
+ | 'accessibilityValue'
117
+ | 'nativeID'
118
+ | 'onAccessibilityAction'
119
+ | 'testID'
120
+ > &
121
+ ReturnType<typeof createParentAccessibilityProps>;
122
+
123
+ type NativeTextRendererProps = Omit<TextMotionRendererProps, 'recipe'> & {
124
+ recipe: TextMotionInternalRecipeConfig;
125
+ };
126
+
127
+ type NativeTextTokenTextProps = Pick<
128
+ TextMotionComponentTextProps,
129
+ 'allowFontScaling' | 'maxFontSizeMultiplier'
130
+ >;
131
+
132
+ type NativeTextTextRenderFragment = {
133
+ kind: 'text';
134
+ key: string;
135
+ renderIndex: number;
136
+ text: string;
137
+ textFragmentIndex: number;
138
+ token: TextMotionToken;
139
+ };
140
+
141
+ type NativeTextLineBreakRenderFragment = {
142
+ kind: 'line-break';
143
+ key: string;
144
+ preservesLineHeight: boolean;
145
+ renderIndex: number;
146
+ };
147
+
148
+ type NativeTextRenderFragment = NativeTextTextRenderFragment | NativeTextLineBreakRenderFragment;
149
+
150
+ type NativeTextTextRenderFragmentDraft = Omit<NativeTextTextRenderFragment, 'renderIndex'>;
151
+ type NativeTextLineBreakRenderFragmentDraft = Omit<
152
+ NativeTextLineBreakRenderFragment,
153
+ 'renderIndex'
154
+ >;
155
+ type NativeTextRenderFragmentDraft =
156
+ | NativeTextTextRenderFragmentDraft
157
+ | NativeTextLineBreakRenderFragmentDraft;
158
+
159
+ export type NativeTextRendererOptions = {
160
+ /** Prefix used for generated token `testID` values. */
161
+ testIDPrefix?: string;
162
+ };
163
+
164
+ const AnimatedView = createAnimatedComponent(View);
165
+
166
+ const TEXT_ALIGN_TO_JUSTIFY_CONTENT = {
167
+ auto: 'flex-start',
168
+ center: 'center',
169
+ justify: 'flex-start',
170
+ left: 'flex-start',
171
+ right: 'flex-end',
172
+ } as const satisfies Record<NonNullable<TextStyle['textAlign']>, ViewStyle['justifyContent']>;
173
+
174
+ const NATIVE_TEXT_LINE_BREAK_PATTERN = /\r\n|\n/g;
175
+ const NATIVE_TEXT_LINE_BREAK_MARKER_STYLE = {
176
+ flexBasis: '100%',
177
+ height: 0,
178
+ width: '100%',
179
+ } as const satisfies ViewStyle;
180
+ const NATIVE_TEXT_BLANK_LINE_SPACER_STYLE = {
181
+ flexBasis: '100%',
182
+ opacity: 0,
183
+ width: '100%',
184
+ } as const satisfies TextStyle;
185
+ const NATIVE_TEXT_BLANK_LINE_SPACER_TEXT = ' ';
186
+ const NATIVE_TEXT_ANIMATED_TOKEN_CONTAINER_STYLE = {
187
+ overflow: 'visible',
188
+ } as const satisfies ViewStyle;
189
+
190
+ function shouldAnimateToken(token: TextMotionToken): boolean {
191
+ return token.text.trim().length > 0;
192
+ }
193
+
194
+ function createMotionIndexByTokenId(tokens: readonly TextMotionToken[]) {
195
+ const motionTokens = tokens.filter(shouldAnimateToken);
196
+ const motionIndexByTokenId = new Map(
197
+ motionTokens.map<[string, number]>((token, motionIndex) => [token.id, motionIndex]),
198
+ );
199
+
200
+ return {
201
+ motionCount: motionTokens.length,
202
+ motionIndexByTokenId,
203
+ };
204
+ }
205
+
206
+ function createTokenDelaySecondsByMotionIndex(
207
+ recipe: NativeTextRendererProps['recipe'],
208
+ count: number,
209
+ ): readonly number[] {
210
+ return createTextMotionDelaySecondsByItemIndex(recipe, count);
211
+ }
212
+
213
+ function createTokenMotion(
214
+ recipe: NativeTextRendererProps['recipe'],
215
+ motionIndex: number,
216
+ styleState: TextMotionStyleTransformStatePair,
217
+ delaySecondsByMotionIndex: readonly number[],
218
+ ): TextMotionItemMotion {
219
+ return createTextMotionItemMotion(
220
+ recipe,
221
+ styleState,
222
+ delaySecondsByMotionIndex[motionIndex] ?? 0,
223
+ );
224
+ }
225
+
226
+ function createControlledProgressPlan(
227
+ motionIndex: number,
228
+ delaySecondsByMotionIndex: readonly number[],
229
+ totalTimelineSpan: number,
230
+ ): TextMotionControlledProgressPlan {
231
+ return createTextMotionControlledProgressPlan(
232
+ motionIndex,
233
+ delaySecondsByMotionIndex,
234
+ totalTimelineSpan,
235
+ );
236
+ }
237
+
238
+ function createNativeTextTextFragment(
239
+ token: TextMotionToken,
240
+ text: string,
241
+ textFragmentIndex: number,
242
+ ): NativeTextTextRenderFragmentDraft {
243
+ return {
244
+ kind: 'text',
245
+ key: textFragmentIndex === 0 ? token.id : `${token.id}:text:${textFragmentIndex}`,
246
+ text,
247
+ textFragmentIndex,
248
+ token,
249
+ };
250
+ }
251
+
252
+ function createNativeTextLineBreakFragment(
253
+ token: TextMotionToken,
254
+ lineBreakIndex: number,
255
+ preservesLineHeight: boolean,
256
+ ): NativeTextLineBreakRenderFragmentDraft {
257
+ return {
258
+ kind: 'line-break',
259
+ key: `${token.id}:line-break:${lineBreakIndex}`,
260
+ preservesLineHeight,
261
+ };
262
+ }
263
+
264
+ function hasNativeTextLineContent(text: string): boolean {
265
+ return text.replace(NATIVE_TEXT_LINE_BREAK_PATTERN, '').length > 0;
266
+ }
267
+
268
+ function hasNativeTextLineContentAfter(sourceText: string, sourceIndex: number): boolean {
269
+ return hasNativeTextLineContent(sourceText.slice(sourceIndex));
270
+ }
271
+
272
+ function createNativeTextTokenRenderFragments(
273
+ token: TextMotionToken,
274
+ sourceText: string,
275
+ currentLineHasText: boolean,
276
+ ): {
277
+ currentLineHasText: boolean;
278
+ fragments: readonly NativeTextRenderFragmentDraft[];
279
+ } {
280
+ if (!token.text.includes('\n')) {
281
+ return {
282
+ currentLineHasText: currentLineHasText || token.text.length > 0,
283
+ fragments: [createNativeTextTextFragment(token, token.text, 0)],
284
+ };
285
+ }
286
+
287
+ const lineBreaks = Array.from(token.text.matchAll(NATIVE_TEXT_LINE_BREAK_PATTERN));
288
+
289
+ const initialAccumulator = {
290
+ cursor: 0,
291
+ currentLineHasText,
292
+ fragments: [] as NativeTextRenderFragmentDraft[],
293
+ textFragmentIndex: 0,
294
+ };
295
+ const result = lineBreaks.reduce((accumulator, lineBreak, lineBreakIndex) => {
296
+ const start = lineBreak.index ?? accumulator.cursor;
297
+ const lineBreakEnd = start + lineBreak[0].length;
298
+ const textBeforeLineBreak = token.text.slice(accumulator.cursor, start);
299
+ const textFragment =
300
+ textBeforeLineBreak.length > 0
301
+ ? [createNativeTextTextFragment(token, textBeforeLineBreak, accumulator.textFragmentIndex)]
302
+ : [];
303
+ const lineHasText = accumulator.currentLineHasText || textBeforeLineBreak.length > 0;
304
+ const hasTextAfterLineBreak = hasNativeTextLineContentAfter(
305
+ sourceText,
306
+ token.sourceRange.start + lineBreakEnd,
307
+ );
308
+
309
+ return {
310
+ cursor: lineBreakEnd,
311
+ currentLineHasText: false,
312
+ fragments: [
313
+ ...accumulator.fragments,
314
+ ...textFragment,
315
+ createNativeTextLineBreakFragment(
316
+ token,
317
+ lineBreakIndex,
318
+ !lineHasText || !hasTextAfterLineBreak,
319
+ ),
320
+ ],
321
+ textFragmentIndex: accumulator.textFragmentIndex + textFragment.length,
322
+ };
323
+ }, initialAccumulator);
324
+ const trailingText = token.text.slice(result.cursor);
325
+
326
+ if (trailingText.length === 0) {
327
+ return {
328
+ currentLineHasText: result.currentLineHasText,
329
+ fragments: result.fragments,
330
+ };
331
+ }
332
+
333
+ return {
334
+ currentLineHasText: true,
335
+ fragments: [
336
+ ...result.fragments,
337
+ createNativeTextTextFragment(token, trailingText, result.textFragmentIndex),
338
+ ],
339
+ };
340
+ }
341
+
342
+ function createNativeTextRenderFragments(
343
+ tokens: readonly TextMotionToken[],
344
+ sourceText: string,
345
+ ): readonly NativeTextRenderFragment[] {
346
+ const result = tokens.reduce(
347
+ (accumulator, token) => {
348
+ const tokenResult = createNativeTextTokenRenderFragments(
349
+ token,
350
+ sourceText,
351
+ accumulator.currentLineHasText,
352
+ );
353
+
354
+ return {
355
+ currentLineHasText: tokenResult.currentLineHasText,
356
+ fragments: [...accumulator.fragments, ...tokenResult.fragments],
357
+ };
358
+ },
359
+ {
360
+ currentLineHasText: false,
361
+ fragments: [] as NativeTextRenderFragmentDraft[],
362
+ },
363
+ );
364
+
365
+ return result.fragments.map((fragment, renderIndex) => ({
366
+ ...fragment,
367
+ renderIndex,
368
+ }));
369
+ }
370
+
371
+ function createNativeTextFragmentTestID(
372
+ prefix: string | undefined,
373
+ fragment: NativeTextTextRenderFragment,
374
+ ): string | undefined {
375
+ if (!prefix) {
376
+ return undefined;
377
+ }
378
+
379
+ if (fragment.textFragmentIndex === 0) {
380
+ return `${prefix}-${fragment.token.index}`;
381
+ }
382
+
383
+ return `${prefix}-${fragment.token.index}-${fragment.textFragmentIndex}`;
384
+ }
385
+
386
+ function createNativeTextLineBreakTestID(
387
+ prefix: string | undefined,
388
+ fragment: NativeTextLineBreakRenderFragment,
389
+ ): string | undefined {
390
+ return prefix ? `${prefix}-line-break-${fragment.renderIndex}` : undefined;
391
+ }
392
+
393
+ function StaticNativeTextToken({ children, style, ...tokenProps }: AnimatedTextProps) {
394
+ return (
395
+ <Text {...tokenProps} style={style}>
396
+ {children}
397
+ </Text>
398
+ );
399
+ }
400
+
401
+ function splitAnimatedTokenProps({
402
+ accessibilityElementsHidden,
403
+ accessible,
404
+ importantForAccessibility,
405
+ testID,
406
+ ...textProps
407
+ }: AnimatedTextProps) {
408
+ return {
409
+ containerProps: {
410
+ accessibilityElementsHidden,
411
+ accessible,
412
+ importantForAccessibility,
413
+ testID,
414
+ } satisfies NativeTextAnimatedTokenContainerProps,
415
+ textProps,
416
+ };
417
+ }
418
+
419
+ function AnimatedNativeTextTokenContent({ children, style, ...tokenProps }: AnimatedTextProps) {
420
+ return (
421
+ <Text {...tokenProps} style={style}>
422
+ {children}
423
+ </Text>
424
+ );
425
+ }
426
+
427
+ function useNativeTextAnimatedStyle(options: NativeTextAnimatedStyleOptions) {
428
+ const { controlled, progress, renderFinalState, tokenMotion } = options;
429
+ const initialOpacity = tokenMotion.initial.opacity;
430
+ const initialScale = tokenMotion.initial.scale;
431
+ const initialTranslateX = tokenMotion.initial.translateX;
432
+ const initialTranslateY = tokenMotion.initial.translateY;
433
+ const targetOpacity = tokenMotion.target.opacity;
434
+ const targetScale = tokenMotion.target.scale;
435
+ const targetTranslateX = tokenMotion.target.translateX;
436
+ const targetTranslateY = tokenMotion.target.translateY;
437
+ const pulseScale = tokenMotion.pulseScale;
438
+ const revealOnStart = tokenMotion.revealOnStart === true;
439
+ const itemDelaySeconds = controlled ? options.controlledProgressPlan.itemDelaySeconds : 0;
440
+ const totalTimelineSpan = controlled ? options.controlledProgressPlan.totalTimelineSpan : 1;
441
+
442
+ return useAnimatedStyle(() => {
443
+ const current = readTextMotionProgressForAnimatedStyle({
444
+ controlledProgressPlan: controlled
445
+ ? {
446
+ itemDelaySeconds,
447
+ totalTimelineSpan,
448
+ }
449
+ : undefined,
450
+ progress,
451
+ renderFinalState,
452
+ });
453
+ const clampedCurrent = clampTextMotionProgress(current);
454
+ const baseScale = initialScale + (targetScale - initialScale) * current;
455
+ const pulseProgress = 1 - Math.abs(clampedCurrent * 2 - 1);
456
+ const pulseMultiplier = 1 + (pulseScale - 1) * pulseProgress;
457
+
458
+ return {
459
+ opacity:
460
+ revealOnStart && current <= 0
461
+ ? 0
462
+ : initialOpacity + (targetOpacity - initialOpacity) * current,
463
+ transform: [
464
+ {
465
+ translateX: initialTranslateX + (targetTranslateX - initialTranslateX) * current,
466
+ },
467
+ {
468
+ translateY: initialTranslateY + (targetTranslateY - initialTranslateY) * current,
469
+ },
470
+ {
471
+ scale: baseScale * pulseMultiplier,
472
+ },
473
+ ],
474
+ };
475
+ }, [
476
+ initialOpacity,
477
+ initialScale,
478
+ initialTranslateX,
479
+ initialTranslateY,
480
+ controlled,
481
+ progress,
482
+ pulseScale,
483
+ renderFinalState,
484
+ revealOnStart,
485
+ targetOpacity,
486
+ targetScale,
487
+ targetTranslateX,
488
+ targetTranslateY,
489
+ itemDelaySeconds,
490
+ totalTimelineSpan,
491
+ ]);
492
+ }
493
+
494
+ function ControlledAnimatedNativeTextToken({
495
+ children,
496
+ controlledProgress,
497
+ controlledProgressPlan,
498
+ style,
499
+ tokenMotion,
500
+ ...tokenProps
501
+ }: ControlledAnimatedNativeTextTokenProps) {
502
+ const reducedMotion = tokenMotion.reducedMotion;
503
+ const reducedMotionEnabled = useReducedMotion();
504
+ const renderFinalState = shouldRenderTextMotionFinalState(reducedMotionEnabled, reducedMotion);
505
+ const { containerProps, textProps } = splitAnimatedTokenProps(tokenProps);
506
+ const animatedStyle = useNativeTextAnimatedStyle({
507
+ controlled: true,
508
+ controlledProgressPlan,
509
+ progress: controlledProgress,
510
+ renderFinalState,
511
+ tokenMotion,
512
+ });
513
+
514
+ return (
515
+ <AnimatedView
516
+ {...containerProps}
517
+ style={[NATIVE_TEXT_ANIMATED_TOKEN_CONTAINER_STYLE, animatedStyle] as StyleProp<ViewStyle>}
518
+ >
519
+ <AnimatedNativeTextTokenContent {...textProps} style={style}>
520
+ {children}
521
+ </AnimatedNativeTextTokenContent>
522
+ </AnimatedView>
523
+ );
524
+ }
525
+
526
+ function UncontrolledAnimatedNativeTextToken({
527
+ children,
528
+ controls,
529
+ playbackText,
530
+ style,
531
+ tokenMotion,
532
+ ...tokenProps
533
+ }: UncontrolledAnimatedNativeTextTokenProps) {
534
+ const reducedMotion = tokenMotion.reducedMotion;
535
+ const reducedMotionEnabled = useReducedMotion();
536
+ const renderFinalState = shouldRenderTextMotionFinalState(reducedMotionEnabled, reducedMotion);
537
+ const progress = useSharedValue(renderFinalState ? 1 : 0);
538
+ const playbackRun = createNativeTextPlaybackRun(tokenMotion, renderFinalState, playbackText);
539
+ const previousPlaybackRun = useRef<NativeTextPlaybackRun | undefined>(undefined);
540
+ const { containerProps, textProps } = splitAnimatedTokenProps(tokenProps);
541
+ const animatedStyle = useNativeTextAnimatedStyle({
542
+ controlled: false,
543
+ progress,
544
+ renderFinalState,
545
+ tokenMotion,
546
+ });
547
+
548
+ useLayoutEffect(() => {
549
+ if (areNativeTextPlaybackRunsEqual(previousPlaybackRun.current, playbackRun)) {
550
+ return;
551
+ }
552
+
553
+ previousPlaybackRun.current = playbackRun;
554
+
555
+ if (playbackRun.renderFinalState) {
556
+ progress.value = 1;
557
+ return;
558
+ }
559
+
560
+ progress.value = 0;
561
+ progress.value = createTextMotionProgressAnimation(playbackRun);
562
+ });
563
+
564
+ useNativeTextControlsPlayback({
565
+ controls,
566
+ createProgressAnimation: createTextMotionProgressAnimation,
567
+ progress,
568
+ renderFinalState,
569
+ tokenMotion,
570
+ });
571
+
572
+ return (
573
+ <AnimatedView
574
+ {...containerProps}
575
+ style={[NATIVE_TEXT_ANIMATED_TOKEN_CONTAINER_STYLE, animatedStyle] as StyleProp<ViewStyle>}
576
+ >
577
+ <AnimatedNativeTextTokenContent {...textProps} style={style}>
578
+ {children}
579
+ </AnimatedNativeTextTokenContent>
580
+ </AnimatedView>
581
+ );
582
+ }
583
+
584
+ function NativeTextToken(props: NativeTextTokenProps) {
585
+ if (!props.tokenMotion) {
586
+ const { tokenMotion: _tokenMotion, ...tokenProps } = props;
587
+
588
+ return <StaticNativeTextToken {...tokenProps} />;
589
+ }
590
+
591
+ if ('controlledProgress' in props) {
592
+ const { controlledProgress, controlledProgressPlan, tokenMotion, ...tokenProps } = props;
593
+
594
+ return (
595
+ <ControlledAnimatedNativeTextToken
596
+ {...tokenProps}
597
+ controlledProgress={controlledProgress}
598
+ controlledProgressPlan={controlledProgressPlan}
599
+ tokenMotion={tokenMotion}
600
+ />
601
+ );
602
+ }
603
+
604
+ const { controls, playbackText, tokenMotion, ...tokenProps } = props;
605
+
606
+ return (
607
+ <UncontrolledAnimatedNativeTextToken
608
+ {...tokenProps}
609
+ controls={controls}
610
+ playbackText={playbackText}
611
+ tokenMotion={tokenMotion}
612
+ />
613
+ );
614
+ }
615
+
616
+ function NativeTextLineBreak({
617
+ preservesLineHeight,
618
+ testID,
619
+ textStyle,
620
+ tokenTextProps,
621
+ }: {
622
+ preservesLineHeight: boolean;
623
+ testID?: string;
624
+ textStyle: StyleProp<TextStyle>;
625
+ tokenTextProps: NativeTextTokenTextProps;
626
+ }) {
627
+ if (preservesLineHeight) {
628
+ return (
629
+ <Text
630
+ {...tokenTextProps}
631
+ accessibilityElementsHidden
632
+ accessible={false}
633
+ importantForAccessibility="no-hide-descendants"
634
+ style={[textStyle, NATIVE_TEXT_BLANK_LINE_SPACER_STYLE]}
635
+ testID={testID}
636
+ >
637
+ {NATIVE_TEXT_BLANK_LINE_SPACER_TEXT}
638
+ </Text>
639
+ );
640
+ }
641
+
642
+ return (
643
+ <View
644
+ accessibilityElementsHidden
645
+ accessible={false}
646
+ importantForAccessibility="no-hide-descendants"
647
+ style={NATIVE_TEXT_LINE_BREAK_MARKER_STYLE}
648
+ testID={testID}
649
+ />
650
+ );
651
+ }
652
+
653
+ function resolveJustifyContent(style: StyleProp<TextStyle>): ViewStyle['justifyContent'] {
654
+ const textAlign = StyleSheet.flatten(style)?.textAlign;
655
+
656
+ return textAlign ? TEXT_ALIGN_TO_JUSTIFY_CONTENT[textAlign] : 'flex-start';
657
+ }
658
+
659
+ function createContainerStyle(style: StyleProp<TextStyle>): StyleProp<ViewStyle> {
660
+ return {
661
+ flexDirection: 'row',
662
+ flexWrap: 'wrap',
663
+ justifyContent: resolveJustifyContent(style),
664
+ };
665
+ }
666
+
667
+ function createContainerProps(
668
+ textProps: TextMotionComponentTextProps | undefined,
669
+ parentAccessibilityProps: ReturnType<typeof createParentAccessibilityProps>,
670
+ ): NativeTextContainerProps {
671
+ return {
672
+ accessibilityActions: textProps?.accessibilityActions,
673
+ accessibilityHint: textProps?.accessibilityHint,
674
+ accessibilityLabel:
675
+ textProps?.accessibilityLabel ?? parentAccessibilityProps.accessibilityLabel,
676
+ accessibilityLanguage: textProps?.accessibilityLanguage,
677
+ accessibilityRole: textProps?.accessibilityRole,
678
+ accessibilityState: textProps?.accessibilityState,
679
+ accessibilityValue: textProps?.accessibilityValue,
680
+ accessible: parentAccessibilityProps.accessible,
681
+ nativeID: textProps?.nativeID,
682
+ onAccessibilityAction: textProps?.onAccessibilityAction,
683
+ testID: textProps?.testID,
684
+ };
685
+ }
686
+
687
+ function createTokenTextProps(
688
+ textProps: TextMotionComponentTextProps | undefined,
689
+ ): NativeTextTokenTextProps {
690
+ return {
691
+ allowFontScaling: textProps?.allowFontScaling,
692
+ maxFontSizeMultiplier: textProps?.maxFontSizeMultiplier,
693
+ };
694
+ }
695
+
696
+ function createNativeTextRendererComponent(
697
+ options: NativeTextRendererOptions,
698
+ ): ComponentType<NativeTextRendererProps> {
699
+ function NativeTextRenderer({ children, recipe, textProps, tokens }: NativeTextRendererProps) {
700
+ const text = typeof children === 'string' ? children : '';
701
+ const textStyle = textProps?.style;
702
+ const policy = recipe.accessibility ?? parentLabelPolicy();
703
+ const accessibilityLabel = resolveAccessibleText(text, tokens);
704
+ const parentAccessibilityProps = createParentAccessibilityProps(policy, accessibilityLabel);
705
+ const tokenAccessibilityProps = createHiddenTokenAccessibilityProps(policy);
706
+ const containerProps = createContainerProps(textProps, parentAccessibilityProps);
707
+ const tokenTextProps = createTokenTextProps(textProps);
708
+ const styleState = createTextMotionStyleTransformStatePair(recipe.effects, 'nativeText()');
709
+ const { motionCount, motionIndexByTokenId } = createMotionIndexByTokenId(tokens);
710
+ const delaySecondsByMotionIndex = createTokenDelaySecondsByMotionIndex(recipe, motionCount);
711
+ const totalTimelineSpan = createTextMotionControlledTimelineSpan(delaySecondsByMotionIndex);
712
+ const externalProgress = textProps?.progress;
713
+ const controls = textProps?.controls;
714
+ const fragments = createNativeTextRenderFragments(tokens, text);
715
+
716
+ if (externalProgress && controls) {
717
+ throw new Error(
718
+ '@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.',
719
+ );
720
+ }
721
+
722
+ return (
723
+ <View {...containerProps} style={createContainerStyle(textStyle)}>
724
+ {fragments.map((fragment) => {
725
+ if (fragment.kind === 'line-break') {
726
+ return (
727
+ <NativeTextLineBreak
728
+ key={fragment.key}
729
+ preservesLineHeight={fragment.preservesLineHeight}
730
+ testID={createNativeTextLineBreakTestID(options.testIDPrefix, fragment)}
731
+ textStyle={textStyle}
732
+ tokenTextProps={tokenTextProps}
733
+ />
734
+ );
735
+ }
736
+
737
+ const token = fragment.token;
738
+ const motionIndex = motionIndexByTokenId.get(token.id);
739
+ const commonTokenProps = {
740
+ ...tokenAccessibilityProps,
741
+ ...tokenTextProps,
742
+ style: textStyle,
743
+ testID: createNativeTextFragmentTestID(options.testIDPrefix, fragment),
744
+ };
745
+
746
+ if (typeof motionIndex !== 'number') {
747
+ return (
748
+ <NativeTextToken {...commonTokenProps} key={fragment.key}>
749
+ {fragment.text}
750
+ </NativeTextToken>
751
+ );
752
+ }
753
+
754
+ const tokenMotion = createTokenMotion(
755
+ recipe,
756
+ motionIndex,
757
+ styleState,
758
+ delaySecondsByMotionIndex,
759
+ );
760
+
761
+ if (!externalProgress) {
762
+ return (
763
+ <NativeTextToken
764
+ {...commonTokenProps}
765
+ controls={controls}
766
+ key={fragment.key}
767
+ playbackText={fragment.text}
768
+ tokenMotion={tokenMotion}
769
+ >
770
+ {fragment.text}
771
+ </NativeTextToken>
772
+ );
773
+ }
774
+
775
+ return (
776
+ <NativeTextToken
777
+ {...commonTokenProps}
778
+ controlledProgress={externalProgress}
779
+ controlledProgressPlan={createControlledProgressPlan(
780
+ motionIndex,
781
+ delaySecondsByMotionIndex,
782
+ totalTimelineSpan,
783
+ )}
784
+ key={fragment.key}
785
+ tokenMotion={tokenMotion}
786
+ >
787
+ {fragment.text}
788
+ </NativeTextToken>
789
+ );
790
+ })}
791
+ </View>
792
+ );
793
+ }
794
+
795
+ NativeTextRenderer.displayName = 'NativeTextRenderer';
796
+
797
+ return NativeTextRenderer;
798
+ }
799
+
800
+ /** Render text as wrapping React Native Text tokens animated by Reanimated. */
801
+ export function nativeText(
802
+ options: NativeTextRendererOptions = {},
803
+ ): TextMotionSourceTokenRenderer<
804
+ 'native-text' | 'style-transform',
805
+ NativeTextRendererProps['recipe']
806
+ > {
807
+ return createTextMotionRendererHandle({
808
+ kind: 'nativeText',
809
+ capabilities: ['native-text', 'style-transform'],
810
+ motionUnit: 'source-token',
811
+ Component: createNativeTextRendererComponent(options),
812
+ });
813
+ }