@react-native-motion-kit/text-motion 0.1.0 → 0.1.1
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.
- package/CHANGELOG.md +8 -5
- package/README.ko.md +47 -641
- package/README.md +46 -639
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,441 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# React Native Text Motion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> English | [한국어](https://github.com/react-native-motion-kit/react-native-text-motion/blob/main/packages/text-motion/README.ko.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/react-native-motion-kit/react-native-text-motion/main/docs/docs/public/logo.png" width="300" alt="React Native Motion Kit logo" />
|
|
7
|
+
</div>
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Overview
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
`@react-native-motion-kit/text-motion` brings composable text animations to React Native with Reanimated. Build reusable animated components for short titles, labels, onboarding copy, and product copy by choosing how text is split, rendered, timed, and animated.
|
|
10
12
|
|
|
11
|
-
|
|
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
|
-
```
|
|
13
|
+
### Key Features
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
15
|
+
- **Composable recipes** — Combine splitting, layout, timelines, and effects with a typed API.
|
|
16
|
+
- **Word and character motion** — Animate words or graphemes with `nativeText()`, including character-by-character `typewriter()` reveals.
|
|
17
|
+
- **Rendered-line reveals** — Use `overlayText()` and `lineReveal()` to animate the lines React Native actually rendered.
|
|
18
|
+
- **Playback control** — Trigger play/replay/reset/stop from events or drive progress with an external Reanimated shared value.
|
|
19
|
+
- **Accessibility** — Read the whole phrase as one label, hide decorative tokens from screen readers, and respect reduced motion by default.
|
|
20
|
+
- **Ready-to-use presets** — Start with a preset, then customize its recipe when needed.
|
|
37
21
|
|
|
38
22
|
## Quick Start
|
|
39
23
|
|
|
40
|
-
|
|
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
|
|
24
|
+
### Documentation
|
|
55
25
|
|
|
56
|
-
|
|
26
|
+
[Full documentation](https://react-native-text-motion.pages.dev/) · [Installation](https://react-native-text-motion.pages.dev/guide/getting-started/installation.html) · [Quick start](https://react-native-text-motion.pages.dev/guide/getting-started/quick-start.html)
|
|
57
27
|
|
|
58
|
-
|
|
59
|
-
import {
|
|
60
|
-
defineTextMotion,
|
|
61
|
-
fade,
|
|
62
|
-
nativeText,
|
|
63
|
-
rise,
|
|
64
|
-
scale,
|
|
65
|
-
stagger,
|
|
66
|
-
words,
|
|
67
|
-
} from '@react-native-motion-kit/text-motion';
|
|
28
|
+
### Examples
|
|
68
29
|
|
|
69
|
-
|
|
70
|
-
|
|
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();
|
|
30
|
+
- [Examples gallery](https://react-native-text-motion.pages.dev/guide/usage/examples.html) — Explore demos and their source code.
|
|
31
|
+
- [Expo example app](https://github.com/react-native-motion-kit/react-native-text-motion/tree/main/example) — Run recipes, playback controls, and native rendered-line examples.
|
|
76
32
|
|
|
77
|
-
|
|
78
|
-
return <ProductHeadline>Design motion that feels native</ProductHeadline>;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
33
|
+
### AI
|
|
81
34
|
|
|
82
|
-
|
|
35
|
+
See [Use with AI tools](https://react-native-text-motion.pages.dev/guide/getting-started/ai.html) for documentation workflows.
|
|
83
36
|
|
|
84
|
-
|
|
37
|
+
- [llms.txt](https://react-native-text-motion.pages.dev/llms.txt) — A structured index of documentation pages.
|
|
38
|
+
- [llms-full.txt](https://react-native-text-motion.pages.dev/llms-full.txt) — The complete documentation in one text file.
|
|
39
|
+
- [Korean index](https://react-native-text-motion.pages.dev/ko/llms.txt) · [Korean full documentation](https://react-native-text-motion.pages.dev/ko/llms-full.txt)
|
|
85
40
|
|
|
86
|
-
|
|
41
|
+
### Installation
|
|
87
42
|
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
.motion({ kind: 'spring', options: { damping: 14, stiffness: 160 } });
|
|
43
|
+
```sh
|
|
44
|
+
pnpm add @react-native-motion-kit/text-motion react-native-reanimated react-native-worklets
|
|
91
45
|
```
|
|
92
46
|
|
|
93
|
-
|
|
47
|
+
The peer minimums are Reanimated `4.0.0` and Worklets `0.5.0`. These minimums are not a complete native compatibility matrix. Follow the [installation guide](https://react-native-text-motion.pages.dev/guide/getting-started/installation.html) and [official Reanimated setup](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) for your React Native or Expo version. Reanimated 4 requires the New Architecture.
|
|
94
48
|
|
|
95
|
-
|
|
49
|
+
### Basic Usage
|
|
96
50
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## Presets
|
|
100
|
-
|
|
101
|
-
Presets are composable recipe factories, not string names.
|
|
51
|
+
Create a component from a preset and pass the text you want to animate:
|
|
102
52
|
|
|
103
53
|
```tsx
|
|
104
|
-
import {
|
|
105
|
-
editorialRise,
|
|
106
|
-
gentleEmphasis,
|
|
107
|
-
softWave,
|
|
108
|
-
} from '@react-native-motion-kit/text-motion/presets';
|
|
54
|
+
import { editorialRise } from '@react-native-motion-kit/text-motion/presets';
|
|
109
55
|
|
|
56
|
+
// Create the reusable component outside render.
|
|
110
57
|
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
58
|
|
|
195
59
|
export function HeroTitle() {
|
|
196
|
-
return <
|
|
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
|
-
);
|
|
60
|
+
return <HeroReveal>Design motion that feels native</HeroReveal>;
|
|
336
61
|
}
|
|
337
62
|
```
|
|
338
63
|
|
|
339
|
-
|
|
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.
|
|
64
|
+
For custom motion, compose a recipe. `stagger()` uses seconds, and `.component()` requires a renderer selected with `.layout(...)`.
|
|
439
65
|
|
|
440
66
|
```tsx
|
|
441
67
|
import {
|
|
@@ -444,254 +70,35 @@ import {
|
|
|
444
70
|
nativeText,
|
|
445
71
|
rise,
|
|
446
72
|
stagger,
|
|
447
|
-
useTextMotionControls,
|
|
448
73
|
words,
|
|
449
74
|
} from '@react-native-motion-kit/text-motion';
|
|
450
|
-
import { Button } from 'react-native';
|
|
451
75
|
|
|
452
|
-
const
|
|
76
|
+
const WordReveal = defineTextMotion()
|
|
453
77
|
.split(words())
|
|
454
78
|
.layout(nativeText())
|
|
455
79
|
.timeline(stagger(0.04))
|
|
456
|
-
.effect(rise({ y: 12 })
|
|
457
|
-
.motion({ kind: 'timing', options: { duration: 420 } })
|
|
80
|
+
.effect(fade().and(rise({ y: 12 })))
|
|
458
81
|
.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
82
|
```
|
|
475
83
|
|
|
476
|
-
`controls`
|
|
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.
|
|
84
|
+
Continue with the [recipe API](https://react-native-text-motion.pages.dev/api/recipe.html), [renderers](https://react-native-text-motion.pages.dev/api/renderers.html), and [playback guide](https://react-native-text-motion.pages.dev/guide/usage/playback.html). Use either `controls` or external `progress` on an instance; they cannot be combined.
|
|
482
85
|
|
|
483
|
-
|
|
86
|
+
## Scope
|
|
484
87
|
|
|
485
|
-
`
|
|
88
|
+
Designed for short UI text, this library does not replace every native `Text` feature. `nativeText()` animates source tokens; `overlayText()` animates actual rendered lines and must not use `.split(...)`. The `lines()` splitter handles explicit `\n` characters only.
|
|
486
89
|
|
|
487
|
-
|
|
90
|
+
Measure long paragraphs and many animated rows on target devices. Browser demos are not native performance evidence. See [performance guidance](https://react-native-text-motion.pages.dev/guide/usage/performance.html) and [accessibility](https://react-native-text-motion.pages.dev/guide/usage/accessibility.html).
|
|
488
91
|
|
|
489
|
-
|
|
92
|
+
## Project direction
|
|
490
93
|
|
|
491
|
-
|
|
94
|
+
Discuss priorities and feature ideas in [Project direction & roadmap](https://github.com/react-native-motion-kit/react-native-text-motion/discussions/10).
|
|
492
95
|
|
|
493
|
-
|
|
96
|
+
Read the [project context and roadmap](https://github.com/react-native-motion-kit/react-native-text-motion/blob/main/ROADMAP.md) for priorities and design boundaries before extending the library.
|
|
494
97
|
|
|
495
|
-
|
|
98
|
+
## Contributing
|
|
496
99
|
|
|
497
|
-
|
|
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
|
-
```
|
|
100
|
+
[Contributing guide and development setup](https://github.com/react-native-motion-kit/react-native-text-motion/blob/main/CONTRIBUTING.md)
|
|
694
101
|
|
|
695
102
|
## License
|
|
696
103
|
|
|
697
|
-
MIT
|
|
104
|
+
[MIT](./LICENSE)
|