@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
package/README.md CHANGED
@@ -1,3 +1,697 @@
1
- # Temporary Holding Version
1
+ # @react-native-motion-kit/text-motion
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ High-performance split text animations for React Native, powered by Reanimated and extensible renderers.
4
+
5
+ Korean documentation: [README.ko.md](./README.ko.md)
6
+
7
+ `text-motion` is built for short titles, labels, onboarding copy, product copy, and other UI text that benefits from token-level motion.
8
+
9
+ It is not a single prop-heavy wrapper like this:
10
+
11
+ ```tsx
12
+ <AnimatedText text="Hello" animation="fadeIn" delay={100} />
13
+ ```
14
+
15
+ Instead, it helps you create reusable animated text components by composing the text splitting strategy, token renderer, timeline, and effects:
16
+
17
+ ```txt
18
+ split -> layout(renderer) -> timeline -> effect -> motion/accessibility -> component
19
+ ```
20
+
21
+ Use a preset when you want a good default. Create a custom recipe when you need to decide how text is split, how tokens are rendered, when each visible token starts, and what each token does.
22
+
23
+ For TypeScript users, `.component()` is only available after a renderer is selected with `.layout(...)`. `.recipe()` can still be used before layout when you need to inspect or pass around an unfinished recipe.
24
+
25
+ ## Installation
26
+
27
+ ```sh
28
+ pnpm add @react-native-motion-kit/text-motion react-native-reanimated react-native-worklets
29
+ ```
30
+
31
+ The MVP peer dependency minimums are:
32
+
33
+ - `react-native-reanimated`: `>=4.0.0`
34
+ - `react-native-worklets`: `>=0.5.0`
35
+
36
+ Follow the official Reanimated and Worklets setup for your app, including the New Architecture requirements of Reanimated 4.
37
+
38
+ ## Quick Start
39
+
40
+ Use a preset when you want a good default with very little code:
41
+
42
+ ```tsx
43
+ import { editorialRise } from '@react-native-motion-kit/text-motion/presets';
44
+
45
+ const HeroReveal = editorialRise().component();
46
+
47
+ export function HeroTitle() {
48
+ return <HeroReveal>Design motion that feels native</HeroReveal>;
49
+ }
50
+ ```
51
+
52
+ `editorialRise()` is a recipe factory. Internally it splits the sentence into words, renders with `nativeText()`, starts near the center, and combines `rise`, `fade`, and subtle `scale`.
53
+
54
+ ## Custom Recipe
55
+
56
+ Create a recipe when you want control:
57
+
58
+ ```tsx
59
+ import {
60
+ defineTextMotion,
61
+ fade,
62
+ nativeText,
63
+ rise,
64
+ scale,
65
+ stagger,
66
+ words,
67
+ } from '@react-native-motion-kit/text-motion';
68
+
69
+ const ProductHeadline = defineTextMotion()
70
+ .split(words())
71
+ .layout(nativeText())
72
+ .timeline(stagger(0.032, { from: 'center' }))
73
+ .effect(rise({ y: 14 }).and(fade()).and(scale({ from: 0.98 })))
74
+ .motion({ kind: 'timing', options: { duration: 320 } })
75
+ .component();
76
+
77
+ export function Header() {
78
+ return <ProductHeadline>Design motion that feels native</ProductHeadline>;
79
+ }
80
+ ```
81
+
82
+ The `0.032` value is seconds. Each visible token starts 32ms after the previous timing slot from the selected origin. The `duration` controls how long each token animates after its own delay begins.
83
+
84
+ ### Motion Options
85
+
86
+ `.motion()` accepts a small discriminated config:
87
+
88
+ ```tsx
89
+ .motion({ kind: 'timing', options: { duration: 320 } });
90
+ .motion({ kind: 'spring', options: { damping: 14, stiffness: 160 } });
91
+ ```
92
+
93
+ `timing` options follow Reanimated's `withTiming` config and `spring` options follow Reanimated's `withSpring` config. `reduceMotion` is intentionally not accepted there because text-motion controls reduced-motion behavior through the accessibility policy.
94
+
95
+ Function-valued options, such as a custom `easing`, are compared by reference when text-motion decides whether an in-flight animation should keep playing or replay. Hoist or reuse the same function reference when a parent rerender should preserve progress; creating a new function is treated as a motion config change.
96
+
97
+ `.motion()` configures playback owned by text-motion. It is used by mount autoplay and by `controls` commands. When a component receives raw `progress`, the app owns playback, so `.motion()` is not applied to that instance.
98
+
99
+ ## Presets
100
+
101
+ Presets are composable recipe factories, not string names.
102
+
103
+ ```tsx
104
+ import {
105
+ editorialRise,
106
+ gentleEmphasis,
107
+ softWave,
108
+ } from '@react-native-motion-kit/text-motion/presets';
109
+
110
+ const HeroReveal = editorialRise().component();
111
+ const ProductCopy = softWave().component();
112
+ const LabelEmphasis = gentleEmphasis().component();
113
+ ```
114
+
115
+ - `editorialRise()` - title reveal that starts near the center and expands outward.
116
+ - `softWave()` - product-copy reveal with sine-wave token delays.
117
+ - `gentleEmphasis()` - small-label emphasis with fade and a midpoint pulse.
118
+
119
+ ## Splitters
120
+
121
+ Splitters decide what a token is.
122
+
123
+ Splitter factories validate their inputs, but the returned splitter is an opaque handle. Pass it to `defineTextMotion().split(...)`; descriptor fields such as `kind` and the internal `split` function are intentionally not part of the root user API.
124
+
125
+ ### Words
126
+
127
+ ```tsx
128
+ const WordReveal = defineTextMotion()
129
+ .split(words())
130
+ .layout(nativeText())
131
+ .timeline(stagger(0.034))
132
+ .effect(fade().and(rise({ y: 10 })))
133
+ .component();
134
+ ```
135
+
136
+ `words()` preserves spacing tokens for rendering, but whitespace does not consume timeline delay slots. In `"Words move as groups"`, the visible words animate as indices `0, 1, 2, 3`.
137
+
138
+ Explicit newline characters are preserved for layout too. For example,
139
+ `"First line\nSecond line"` renders as two hard-broken visual lines with
140
+ `nativeText()`, while the newline itself still does not consume a motion index.
141
+ This is different from measuring automatic React Native line wrapping.
142
+
143
+ `nativeText()` is meant for split motion in titles, labels, onboarding copy, and
144
+ short product copy. Animated tokens use an animated container with an inner
145
+ `Text`; generated token `testID` values identify that container, while text
146
+ rendering props such as `allowFontScaling` are forwarded to the inner `Text`.
147
+ It is not a full React Native `Text` drop-in, and very long grapheme animations
148
+ remain a stress case rather than a stable performance promise.
149
+
150
+ ### Graphemes
151
+
152
+ ```tsx
153
+ const GlyphReveal = defineTextMotion()
154
+ .split(graphemes())
155
+ .layout(nativeText())
156
+ .timeline(stagger(0.014, { from: 'center' }))
157
+ .effect(fade().and(scale({ from: 0.86 })))
158
+ .component();
159
+ ```
160
+
161
+ `graphemes()` is character-level motion for user-perceived characters. It is safer than `text.split('')` for accents, Korean, emoji sequences, CJK, and RTL samples.
162
+
163
+ ### Lines
164
+
165
+ ```tsx
166
+ const LineReveal = defineTextMotion()
167
+ .split(lines())
168
+ .layout(nativeText())
169
+ .timeline(stagger(0.12))
170
+ .effect(slide({ y: 16 }).and(fade()))
171
+ .component();
172
+ ```
173
+
174
+ `lines()` is experimental and newline-only. It splits on explicit `\n` characters. It does not measure automatic React Native line wrapping.
175
+
176
+ ### Rendered Line Reveal
177
+
178
+ Use `overlayText()` with `lineReveal()` when the motion unit should be the line React Native actually rendered after width, font, font scale, alignment, language shaping, and wrapping are applied.
179
+
180
+ ```tsx
181
+ import {
182
+ defineTextMotion,
183
+ lineReveal,
184
+ overlayText,
185
+ stagger,
186
+ } from '@react-native-motion-kit/text-motion';
187
+
188
+ const HeroLines = defineTextMotion()
189
+ .layout(overlayText())
190
+ .timeline(stagger(0.08))
191
+ .effect(lineReveal({ y: 18 }))
192
+ .motion({ kind: 'timing', options: { duration: 420 } })
193
+ .component();
194
+
195
+ export function HeroTitle() {
196
+ return <HeroLines>Design motion that feels native</HeroLines>;
197
+ }
198
+ ```
199
+
200
+ Do not call `.split(...)` with `overlayText()`. `overlayText()` owns rendered lines after layout, while `nativeText()` consumes source word, grapheme, custom, or explicit newline tokens. TypeScript removes incompatible chains, and runtime capability checks reject unsafe JavaScript or casted combinations.
201
+
202
+ Use rendered line reveal for responsive hero titles, marketing headings, onboarding headlines, and concise product copy where the final visual line breaks matter. Use `nativeText()` for word or grapheme motion. Use `lines()` only when authored `\n` breaks are the token boundary you want; it does not discover automatic wrapping.
203
+
204
+ `overlayText()` also accepts the built-in style-transform effects (`fade`, `rise`, `slide`, `shake`, `scale`, and `pulse`) because they apply to each rendered line frame. `lineReveal()` is split internally: opacity belongs to the line frame, while the vertical reveal offset moves only the paragraph copy inside the line mask. This lets `lineReveal().and(scale())`, larger finite `scale()` values, and `pulse()` grow from the measured line center without being clipped by the renderer's own mask.
205
+
206
+ Blank and whitespace-only rendered lines are preserved as visual spacing, but they do not consume a motion index. A title such as `"First\n\nSecond"` keeps the blank line while the two nonblank lines reveal in adjacent stagger slots.
207
+
208
+ Relayout behavior is intentionally predictable:
209
+
210
+ - identical line measurement does not replay
211
+ - geometry-only relayout preserves the current run
212
+ - a line topology change restarts component-owned playback once
213
+ - external `progress` keeps the app-owned shared value and applies it to the new geometry
214
+ - reduced motion renders final readable source text without measurement or playback overlays
215
+ - malformed or unsupported native geometry falls back to readable final source text
216
+
217
+ `overlayText()` creates one full paragraph copy per moving rendered line. Performance scales with visible rendered lines, not source word or grapheme count. Treat it as a title, heading, and product-copy renderer. Do not publish measured performance claims until the same case family has iOS and Android evidence.
218
+
219
+ Before valid line geometry exists, the source paragraph remains responsible for layout. When the initial scale is exactly `1`, the pending source keeps the same combined initial opacity and transform users already see from other renderers. When the initial scale is not exactly `1`, the pending source is visually hidden until geometry is valid so the first visible scaled frame uses the measured line center.
220
+
221
+ The renderer fixes self-clipping caused by its own line mask. It does not reserve extra layout space for large transforms and it cannot bypass clipping from a parent or ancestor with `overflow: hidden`; treat those as normal React Native transform limitations.
222
+
223
+ ### Custom
224
+
225
+ ```tsx
226
+ const CustomReveal = defineTextMotion()
227
+ .split(custom((input) => input.match(/\S+|\s+/g) ?? []))
228
+ .layout(nativeText())
229
+ .timeline(stagger(0.028, { from: 'edges' }))
230
+ .effect(rise({ y: 8 }).and(fade()))
231
+ .component();
232
+ ```
233
+
234
+ Use `custom()` for product-specific token boundaries such as hashtags, mentions, prices, punctuation groups, or domain-specific terms. A custom splitter may return strings or token objects:
235
+
236
+ ```tsx
237
+ custom((input) => [
238
+ {
239
+ text: input,
240
+ metadata: { kind: 'headline' },
241
+ sourceRange: { start: 0, end: input.length },
242
+ },
243
+ ]);
244
+ ```
245
+
246
+ If your custom splitter returns plain strings, `text-motion` finds each token in the original text from left to right. This is enough for common splitters such as `input.match(...)`.
247
+
248
+ If your splitter returns tokens out of order, repeats the same text intentionally, or needs exact original positions, return token objects with `sourceRange` instead of plain strings.
249
+
250
+ ## Timelines
251
+
252
+ Timelines calculate per-token start delays in seconds.
253
+
254
+ ```tsx
255
+ stagger(0.04);
256
+ stagger(0.04, { from: 'center' });
257
+ stagger(0.04, { from: 'end' });
258
+ stagger(0.04, { from: 'edges' });
259
+ wave({ amplitude: 0.06, wavelength: 5 });
260
+ sequence(stagger(0.02), wave({ amplitude: 0.04, wavelength: 4 }));
261
+ parallel(stagger(0.08), wave({ amplitude: 0.05, wavelength: 3 }));
262
+ ```
263
+
264
+ - `stagger(step)` - fixed delay step from an origin.
265
+ - `wave()` - sine-wave delay pattern across token indices.
266
+ - `sequence()` - adds multiple timeline delays together.
267
+ - `parallel()` - uses the earliest delay from multiple timelines.
268
+
269
+ Timeline factories validate their input options, but the returned timeline is an opaque handle. Pass it to `.timeline(...)`; descriptor fields such as `delayFor`, `name`, and `options` are intentionally not part of the root user API.
270
+
271
+ Numeric timeline inputs must be finite. `stagger()` accepts a non-negative step, `wave()` accepts a non-negative amplitude, and `wave()` requires `wavelength > 0`.
272
+
273
+ ## Effects
274
+
275
+ Effects describe how each token moves.
276
+
277
+ ```tsx
278
+ fade();
279
+ rise({ y: 14 });
280
+ slide({ x: -12, y: 8 });
281
+ scale({ from: 0.92, to: 1 });
282
+ pulse({ scale: 1.08 });
283
+ shake({ x: 6 });
284
+ lineReveal({ y: 18 });
285
+ typewriter();
286
+ ```
287
+
288
+ Effects compose with `.and(...)`:
289
+
290
+ ```tsx
291
+ rise({ y: 14 }).and(fade()).and(scale({ from: 0.98 }));
292
+ ```
293
+
294
+ `pulse()` is a midpoint emphasis effect. It scales each token up near the middle of its animation and returns to the composed final scale. Use `scale()` when the text should start or end at a different size.
295
+
296
+ Effect factories validate their input options, but the returned effect is an opaque handle. Compose effects with `.and(...)`; descriptor fields such as `name`, `options`, and `requiredCapabilities` are intentionally hidden from the root user API.
297
+
298
+ Numeric built-in effect inputs must be finite numbers. Negative offsets are allowed where the option represents direction, such as `rise({ y })`, `slide({ x, y })`, and `shake({ x })`.
299
+
300
+ `lineReveal()` requires the `line-mask` capability from `overlayText()`. It is not compatible with `nativeText()`.
301
+
302
+ ## Typewriter
303
+
304
+ Use `typewriter()` for short greetings, onboarding copy, or labels that should appear one character at a time while keeping the completed phrase's space and line breaks in place. It is an effect imported from the package root, with no options; it is not a preset or a separate renderer.
305
+
306
+ ```tsx
307
+ import {
308
+ defineTextMotion,
309
+ graphemes,
310
+ nativeText,
311
+ parentLabelPolicy,
312
+ stagger,
313
+ typewriter,
314
+ useTextMotionControls,
315
+ } from '@react-native-motion-kit/text-motion';
316
+ import { Button } from 'react-native';
317
+
318
+ const TypewriterText = defineTextMotion()
319
+ .split(graphemes())
320
+ .layout(nativeText())
321
+ .timeline(stagger(0.04))
322
+ .effect(typewriter())
323
+ .motion({ kind: 'timing', options: { duration: 0 } })
324
+ .accessibility(parentLabelPolicy({ reducedMotion: 'final-state' }))
325
+ .component();
326
+
327
+ export function Greeting() {
328
+ const controls = useTextMotionControls();
329
+
330
+ return (
331
+ <>
332
+ <TypewriterText controls={controls}>안녕 세계! 👩🏽‍💻</TypewriterText>
333
+ <Button title="Replay" onPress={controls.replay} />
334
+ </>
335
+ );
336
+ }
337
+ ```
338
+
339
+ `stagger(0.04)` uses seconds, so visible characters start 40ms apart. The first character appears on the first playback update; whitespace and hard newlines keep their layout space without taking a delay slot, and punctuation takes a slot like any other character. Actual display times follow device frames. `duration: 0` makes the default reveal instant for each character in autoplay and controls-driven playback.
340
+
341
+ Use the existing controls to replay, reset, or stop typing. `stop()` keeps the current state; `play()` afterward does not promise an exact continuation of the remaining typing schedule. To type a different phrase completely from the start, remount it with a new React `key`. Appending text is not a streaming API.
342
+
343
+ For external playback, pass `progress={progress}` instead of `controls`. `.motion()` is ignored for that instance. **Progress describes the effect timeline, not the percentage of characters shown:** with ten visible characters and `stagger(0.05)`, all characters are shown once progress exceeds about `0.310345`, so `0.5` does not mean five characters. Decreasing progress can hide characters again. Use a linear external timing curve when you want evenly spaced threshold crossings in time.
344
+
345
+ `typewriter()` can combine with `rise()` or `fade()`. Each character stays hidden until its local progress becomes positive, then the other effects continue normally; adding `fade()` intentionally makes its appearance gradual. The visibility rule works in either composition order. For a visible movement after each character appears, use a nonzero `.motion()` duration.
346
+
347
+ This effect supports `nativeText()` only. It preserves that renderer's completed token layout; it does not provide exact single-`Text` shaping or line wrapping, a cursor, growing/deleting the underlying string, or streaming. The built-in grapheme fallback now keeps decomposed Hangul syllable sequences such as `한` together without normalizing the source, but it does not implement all Unicode grapheme rules. Complex scripts can still need an app-provided `Intl.Segmenter` or `custom()` splitter. Measure long text and many rows on your target devices before treating them as supported workloads.
348
+
349
+ ### Choosing the reveal and motion
350
+
351
+ `typewriter()` does not choose the splitter or own typing speed. Keep `graphemes()` for character-by-character motion, or use `words()` when whole words should appear together. The timeline owns the start delays, and `.motion()` owns each token's optional motion in autoplay or controls mode. A different stagger origin or `wave()` follows that timeline's order rather than promising start-to-end typing.
352
+
353
+ For example, this effect and motion pair makes each character appear and then rise for 120ms:
354
+
355
+ ```tsx
356
+ .effect(typewriter().and(rise({ y: 6 })))
357
+ .motion({ kind: 'timing', options: { duration: 120 } })
358
+ ```
359
+
360
+ Combining with `fade()` preserves its opacity animation, while Typewriter still prevents characters from showing before they start. Adding `typewriter()` twice does not create a second reveal or another playback clock.
361
+
362
+ With external `progress`, `0` hides the animated characters and `1` renders the final state. The existing mapping uses a token span of `1.0`: for a token delay `d` and the largest delay `D`, local progress is `clamp(progress * (D + 1) - d, 0, 1)`. A character is shown when that local value is greater than zero. There is no remembered maximum progress, so moving backward past a character's start hides it again. A spring overshooting `1` alone does not hide an already started character; it must move back across the character's start threshold.
363
+
364
+ The example's `parentLabelPolicy({ reducedMotion: 'final-state' })` keeps the complete sentence readable when reduced motion is enabled, including an externally controlled instance whose progress is still `0`. The parent exposes the full phrase once to screen readers and the decorative character nodes remain hidden; no per-character announcements are emitted.
365
+
366
+ ## Native Renderer Contract
367
+
368
+ `nativeText()` is the stable MVP renderer. Animated tokens use an animated `View` container with an inner `Text` so transform effects such as `rise()`, `slide()`, `scale()`, and `pulse()` are visible in React Native. Static tokens still render as plain `Text`.
369
+
370
+ `nativeText()` returns an opaque renderer handle for `.layout(...)`. Descriptor fields such as `kind`, `capabilities`, and the implementation `Component` are intentionally hidden from the root user API.
371
+
372
+ This is not a full React Native `Text` drop-in. It favors reliable per-token transforms over exact platform text layout. Use `overlayText()` when you need motion by actual rendered line. Full RN line-to-token mapping remains deferred.
373
+
374
+ Components created with `nativeText()` intentionally support a narrow prop surface:
375
+
376
+ - `style`
377
+ - `testID`
378
+ - `nativeID`
379
+ - `allowFontScaling`
380
+ - `maxFontSizeMultiplier`
381
+ - `controls`
382
+ - `progress`
383
+ - `accessibilityLabel`, `accessibilityHint`, `accessibilityRole`, `accessibilityState`, `accessibilityValue`, `accessibilityActions`, `accessibilityLanguage`, and `onAccessibilityAction`
384
+
385
+ Layout and interaction props that require a single native text node are not part of the stable MVP contract. This includes `numberOfLines`, `ellipsizeMode`, `lineBreakMode`, `onTextLayout`, `selectable`, and text `onPress` handlers.
386
+
387
+ Use `nativeText({ testIDPrefix: 'word' })` when tests need stable per-token `testID` values. Generated token `testID` values identify the animated token container, while text rendering props such as `allowFontScaling` and `maxFontSizeMultiplier` are forwarded to the inner `Text`:
388
+
389
+ ```tsx
390
+ const TestableReveal = defineTextMotion()
391
+ .split(words())
392
+ .layout(nativeText({ testIDPrefix: 'word' }))
393
+ .effect(fade())
394
+ .component();
395
+ ```
396
+
397
+ ### Renderer Performance Thresholds
398
+
399
+ `nativeText()` is the default renderer for high-value UI text: titles, labels, onboarding copy, product copy, and short strings where token motion is easy to see. It is not designed to make long paragraphs or hundreds of grapheme tokens free.
400
+
401
+ The example app has a `Playback -> Renderer Performance` probe for checking the practical boundary. It includes word, grapheme, shared-row, replay, and progress cases. Use it with a real FPS source such as React Native Perf Monitor, Flashlight, Xcode Instruments, or Android Studio profiler.
402
+
403
+ Use these terms when recording results:
404
+
405
+ - `measured`: the same case family has iOS and Android evidence.
406
+ - `provisional`: only one platform, simulator-only, emulator-only, or dev-mode-only evidence exists.
407
+ - `unknown`: the case family was not measured.
408
+
409
+ The decision rule is intentionally conservative. If normal title, label, and product-copy workloads are smooth, keep `nativeText()` as the default renderer and document any grapheme-heavy boundary as guidance. If normal workloads jank, open a renderer optimization plan before adding more effects. If only stress grapheme cases fail, document the stress boundary instead of treating that as proof that the default renderer is wrong.
410
+
411
+ ## Playback Lifecycle
412
+
413
+ The default lifecycle is automatic and input-driven.
414
+
415
+ - Initial mount autoplays animated visible tokens from their initial style to their target style.
416
+ - Parent rerenders with the same text and same recipe preserve in-flight progress.
417
+ - Text changes replay the affected token animation, even when the token shape is the same.
418
+ - Effect, timeline, or motion config changes reset affected animated tokens and autoplay with the updated inputs.
419
+ - Whitespace/static tokens preserve layout text but do not animate or consume motion index.
420
+ - `parentLabelPolicy({ reducedMotion: 'final-state' })` keeps reduced-motion users at the final style without flashing through the initial animated state.
421
+
422
+ Text changes are enter-only in the MVP. The old text is not kept around for an exit animation, crossfade, or token diff. If a component has `controls`, later commands target the currently rendered text. If a component has `progress`, the app-owned shared value decides the new text's visual state; for example, a new phrase rendered while `progress.value` is `0.5` appears at the corresponding mid-state instead of starting autoplay.
423
+
424
+ The example app may remount its demo player to make repeated inspection convenient. Treat that as demo UI behavior, not the recommended app API.
425
+
426
+ ### Playback Controls
427
+
428
+ Use `controls` when an event should tell a text motion component to play, replay, reset, or stop.
429
+
430
+ Common examples:
431
+
432
+ - a headline reveals after the user taps a button
433
+ - onboarding copy advances with the current step
434
+ - a screen title replays when a screen receives focus
435
+ - a label appears only after a form field becomes valid
436
+ - a hero title and subtitle replay together from one control
437
+
438
+ In these cases, the app owns the event, but the text component still owns playback execution. The component uses its recipe, timeline, effects, and `.motion()` config.
439
+
440
+ ```tsx
441
+ import {
442
+ defineTextMotion,
443
+ fade,
444
+ nativeText,
445
+ rise,
446
+ stagger,
447
+ useTextMotionControls,
448
+ words,
449
+ } from '@react-native-motion-kit/text-motion';
450
+ import { Button } from 'react-native';
451
+
452
+ const ReplayableReveal = defineTextMotion()
453
+ .split(words())
454
+ .layout(nativeText())
455
+ .timeline(stagger(0.04))
456
+ .effect(rise({ y: 12 }).and(fade()))
457
+ .motion({ kind: 'timing', options: { duration: 420 } })
458
+ .component();
459
+
460
+ export function Headline() {
461
+ const controls = useTextMotionControls();
462
+
463
+ return (
464
+ <>
465
+ <ReplayableReveal controls={controls}>
466
+ Replay without remounting
467
+ </ReplayableReveal>
468
+
469
+ <Button title="Replay" onPress={controls.replay} />
470
+ <Button title="Reset" onPress={controls.reset} />
471
+ </>
472
+ );
473
+ }
474
+ ```
475
+
476
+ `controls` is a command channel, not a progress value:
477
+
478
+ - `play()` moves from the current progress toward the final state.
479
+ - `replay()` resets to the initial state and plays again with timeline delays.
480
+ - `reset()` cancels playback and returns to the initial state.
481
+ - `stop()` cancels playback and keeps the current visual progress.
482
+
483
+ One controls object may be passed to multiple text motion components. Commands broadcast to every attached component, and each component executes the command with its own recipe.
484
+
485
+ `controls` and `progress` cannot be used together. Use `controls` for discrete events such as buttons and screen focus. Use `progress` when text should follow a continuous value.
486
+
487
+ Text Motion intentionally does not provide a context/provider playback API or a public component ref API. Pass controls explicitly with `controls={controls}` so the connection is visible in JSX.
488
+
489
+ Performance note: controls are designed for short UI text. For a title, label, or short product sentence, playback work should stay small and predictable. For long paragraphs, dense lists, or grapheme-split text with many characters, check the `Renderer Performance` probe and measure on your target device before using it in production.
490
+
491
+ The renderer internals may change as larger workloads become important, so treat the example stress case as a profiling aid rather than a public promise about how controls are implemented.
492
+
493
+ ### Raw Progress
494
+
495
+ Use `progress` when text motion should follow a raw value the app already owns.
496
+
497
+ Common examples:
498
+
499
+ - text follows a scroll or gesture shared value
500
+ - several UI elements need to move from the same shared progress value
501
+ - a custom Reanimated sequence should drive the phrase from `0` to `1`
502
+
503
+ In these cases, the text component should not decide when to start or how the value moves. The app owns a Reanimated shared value and passes it to the text component.
504
+
505
+ ```tsx
506
+ import {
507
+ defineTextMotion,
508
+ fade,
509
+ nativeText,
510
+ rise,
511
+ stagger,
512
+ words,
513
+ } from '@react-native-motion-kit/text-motion';
514
+ import { Button } from 'react-native';
515
+ import { useSharedValue, withTiming } from 'react-native-reanimated';
516
+
517
+ const ProgressReveal = defineTextMotion()
518
+ .split(words())
519
+ .layout(nativeText())
520
+ .timeline(stagger(0.08))
521
+ .effect(rise({ y: 12 }).and(fade()))
522
+ .component();
523
+
524
+ export function Headline() {
525
+ const progress = useSharedValue(0);
526
+
527
+ return (
528
+ <>
529
+ <ProgressReveal progress={progress}>
530
+ Progress drives the whole reveal
531
+ </ProgressReveal>
532
+
533
+ <Button
534
+ title="Play"
535
+ onPress={() => {
536
+ progress.value = withTiming(1, { duration: 720 });
537
+ }}
538
+ />
539
+ </>
540
+ );
541
+ }
542
+ ```
543
+
544
+ `progress` is normalized whole-text progress:
545
+
546
+ - `0` renders the initial token styles.
547
+ - `1` renders every animated token at its final style.
548
+ - Values outside `0..1` are clamped.
549
+ - Non-finite values are treated as `0`.
550
+
551
+ Think of `progress` as the progress of the whole phrase, not of one word. If you use `stagger(0.08)`, the first word can already be finished while the later words are still catching up. The renderer maps the global shared value to each visible token's local progress, so `stagger()`, `wave()`, `sequence()`, and `parallel()` keep their timing shape in controlled mode. Whitespace is still rendered but does not consume a motion index.
552
+
553
+ When `progress` is provided, the app owns playback and `.motion()` does not start internal autoplay. Choose the playback curve where you update the shared value:
554
+
555
+ ```tsx
556
+ progress.value = withTiming(1, { duration: 720 });
557
+ progress.value = withSpring(1);
558
+ progress.value = 0;
559
+ ```
560
+
561
+ If this rendered component receives `progress`, `.motion()` is not applied to that instance. Add `.motion()` only for autoplay or controls-driven versions of the same recipe.
562
+
563
+ Important limitation: controlled mode currently uses a fixed token timeline span of `1.0` inside the whole-text progress mapping. In practice that means timeline delays shape when each token starts, while your `withTiming` or `withSpring` call shapes how fast the entire phrase moves from `0` to `1`. If a future version needs per-token span or duration controls in controlled mode, that should be added as an explicit API instead of silently borrowing `.motion()` duration.
564
+
565
+ First-class `pause`, `seek`, `reverse`, screen focus, in-view, scroll, and gesture drivers remain deferred.
566
+
567
+ ## Accessibility
568
+
569
+ The default renderer uses a parent accessible label and hides decorative animated token nodes, so screen readers read the phrase once instead of token by token.
570
+
571
+ ```tsx
572
+ import { parentLabelPolicy } from '@react-native-motion-kit/text-motion';
573
+
574
+ const ReducedReveal = defineTextMotion()
575
+ .split(words())
576
+ .layout(nativeText())
577
+ .effect(rise().and(fade()))
578
+ .accessibility(parentLabelPolicy({ reducedMotion: 'final-state' }))
579
+ .component();
580
+ ```
581
+
582
+ ## React Native Text Segmentation
583
+
584
+ React Native/Hermes does not guarantee `Intl.Segmenter` in every runtime. The built-in splitters detect it first and use it when available. When it is missing, `graphemes()` and `words()` fall back to a small internal tokenizer so animations do not crash at app startup.
585
+
586
+ The grapheme fallback keeps decomposed Hangul syllable sequences such as `한` together, including syllable boundaries involving precomposed Hangul and Jamo, without normalizing or rewriting the source text. It does not implement every Unicode grapheme rule: some Indic conjuncts such as `क्ष` can still split. Per-token rendering also does not guarantee the shaping or ligatures of a single native `Text`.
587
+
588
+ The fallback is designed for resilient UI motion, not full ICU-level locale segmentation. Apps that need exact locale-aware word boundaries, especially for scripts with complex word breaking, should load an `Intl.Segmenter` polyfill such as `@formatjs/intl-segmenter` in the app entrypoint before rendering text motion components, or pass a `custom()` splitter for product-specific tokenization.
589
+
590
+ ## Stable MVP
591
+
592
+ - `defineTextMotion()`
593
+ - `graphemes()`, `words()`, `custom()`
594
+ - experimental newline-only `lines()`
595
+ - `nativeText()`, `overlayText()`
596
+ - `stagger()`, `sequence()`, `parallel()`, `wave()`
597
+ - `fade()`, `rise()`, `slide()`, `scale()`, `pulse()`, `shake()`, `lineReveal()`, `typewriter()`
598
+ - `useTextMotionControls()` with explicit `controls` prop for event-driven play/replay/reset/stop
599
+ - controlled `progress` via external Reanimated shared values
600
+ - parent-label accessibility policy with hidden animated token nodes
601
+ - `/presets` subpath recipe factories
602
+
603
+ Custom effect factories and renderer capability factories are intentionally not exported from the root MVP API. `nativeText()` only implements the built-in native text effects listed above. A stable renderer-extension SDK will be designed before those helpers become public.
604
+
605
+ ## Deferred
606
+
607
+ These are intentionally not stable exports in the MVP:
608
+
609
+ - custom effect factory API
610
+ - renderer capability factory API
611
+ - `wipe`
612
+ - `scramble`
613
+ - Skia renderer or Skia-only effects
614
+ - playback APIs such as `pause`, `seek`, or `reverse`
615
+ - first-class screen focus, in-view, scroll, or gesture drivers
616
+ - RN-rendered line-to-token mapping
617
+
618
+ Skia remains an optional future package boundary, not a dependency of this core package.
619
+
620
+ Context/provider playback wiring and public component ref playback are not deferred features. They are intentionally outside this package's API shape because they hide ownership and make remount/lifecycle behavior easier to misuse. Use explicit `controls={controls}` wiring when one or more text components should respond to event-driven playback commands.
621
+
622
+ ## Scope & Roadmap
623
+
624
+ Text Motion is a split text motion engine, not a generic animation package. The core question for this package is:
625
+
626
+ ```txt
627
+ Does this feature improve something specific to split text: splitting, layout mapping, token timing, text effects, renderer capability, accessibility, or token-level playback?
628
+ ```
629
+
630
+ If the answer is yes, it may belong in `@react-native-motion-kit/text-motion`.
631
+
632
+ Core text-motion work should stay focused on:
633
+
634
+ - splitters, token metadata, and layout-aware text motion
635
+ - native text renderer reliability and performance
636
+ - timeline, effect, motion, controls, and progress composition for split tokens
637
+ - accessibility behavior for decorative token animation
638
+ - presets that help users ship common title, label, and product-copy motion faster
639
+
640
+ Advanced text effects can be considered when they still depend on text tokens or text layout:
641
+
642
+ - scramble, wipe, and highlight sweep effects
643
+ - word-in-line effects and RN-rendered line-to-token mapping when source range policies are stable enough to document
644
+ - text-change transitions where old and new token sets need a clear playback policy
645
+
646
+ Renderer-specific effects should stay behind renderer capability boundaries:
647
+
648
+ - blur, glow, shader text, masks, and glyph distortion are better candidates for an optional Skia renderer package than the core native text renderer
649
+ - native-only effects should not silently accept renderer-specific options they cannot support
650
+
651
+ Out-of-scope features are likely better served by separate package candidates when they solve a different problem:
652
+
653
+ - number count-up, odometer, currency, percentage, timer, and delta animations are value-formatting problems, not split-text layout problems
654
+ - generic `View` animation, scene animation, and Lottie-style workflows belong outside this package
655
+ - a separate value-motion package could share the Motion Kit philosophy without expanding the text-motion API surface
656
+
657
+ The decision rule is intentionally conservative: add a feature here only when it improves text-specific production work without making the common recipe API harder to understand. Otherwise, keep it deferred, renderer-specific, or a separate package.
658
+
659
+ ## Should You Use It?
660
+
661
+ This package is intentionally small in the MVP. It is best suited for short, high-value UI text where split motion improves the experience without taking over layout.
662
+
663
+ Use it today for:
664
+
665
+ - hero titles, section titles, short labels, onboarding copy, and product descriptions
666
+ - word or grapheme entrance motion with `fade`, `rise`, `slide`, `scale`, `pulse`, or `shake`
667
+ - character-by-character Typewriter reveals that keep the completed phrase's layout
668
+ - reusable recipe components created from presets or `defineTextMotion()`
669
+ - event-driven replay/reset/stop controls without remounting the component
670
+ - raw progress with an external Reanimated shared value
671
+ - rendered-line reveal for responsive hero titles, headings, and concise product copy
672
+ - apps that already use Reanimated 4 and can follow its Worklets setup
673
+ - accessible decorative text motion where the full phrase should remain readable once
674
+
675
+ Wait for a later version if your feature depends on:
676
+
677
+ - playback APIs such as `pause`, `seek`, or `reverse`
678
+ - scroll progress, gesture progress, or viewport/in-view triggers as first-class drivers
679
+ - per-token line measurement or mixed word-in-line animation
680
+ - Skia-only visual effects such as blur, glow, shaders, masks, or glyph distortion
681
+ - rich nested text, inline links, selectable text, or exact native `Text` behavior
682
+ - long paragraphs or many animated rows without measuring performance in your target app
683
+
684
+ The next product focus is verifying line renderer performance and platform behavior, then deciding whether state-transition props, `pause`/`seek`/`reverse`, screen focus helpers, in-view helpers, scroll/gesture drivers, word-in-line effects, or Skia renderer candidates deserve first-class APIs. Items listed under Deferred are not release promises. They should move into the stable API only when the behavior, examples, tests, and documentation are ready.
685
+
686
+ ## Development
687
+
688
+ ```sh
689
+ corepack pnpm install
690
+ corepack pnpm run typecheck
691
+ corepack pnpm run test
692
+ corepack pnpm run build
693
+ ```
694
+
695
+ ## License
696
+
697
+ MIT