fetta 1.4.2 → 1.4.4

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/README.md CHANGED
@@ -6,11 +6,11 @@ Split text into characters, words, and lines while preserving the original typog
6
6
 
7
7
  ## Features
8
8
 
9
- - **Kerning Compensation** — Measures kerning between character pairs, applies margin adjustments to maintain original spacing
10
- - **Nested Elements** — Preserves inline HTML elements (`<a>`, `<em>`, `<strong>`, etc.) with all attributes intact
11
- - **Line Detection** — Detects lines based on Y-position clustering, works with any container width
12
- - **Dash Handling** — Allows text to wrap naturally after em-dashes, en-dashes, and hyphens
13
- - **Auto Re-split** — Automatically re-splits on container resize with debouncing
9
+ - **Kerning Compensation** — Maintains original character spacing when splitting by chars
10
+ - **Nested Elements** — Preserves `<a>`, `<em>`, `<strong>` and other inline elements with all attributes
11
+ - **Line Detection** — Automatically groups words into lines
12
+ - **Dash Handling** — Allows text to wrap naturally after em-dashes, en-dashes, hyphens, and slashes
13
+ - **Auto Re-split** — Re-splits on container resize
14
14
  - **Auto-Revert** — Restore original HTML after animations
15
15
  - **Masking** — Wrap elements in clip containers for reveal animations
16
16
  - **Emoji Support** — Properly handles compound emojis and complex Unicode characters
@@ -26,6 +26,8 @@ Split text into characters, words, and lines while preserving the original typog
26
26
  npm install fetta
27
27
  ```
28
28
 
29
+ **Bundle size**: ~3.9 kB (`fetta/core`) / ~4.8 kB (`fetta/react`) — minified + compressed
30
+
29
31
  ## Quick Start
30
32
 
31
33
  ### Vanilla JavaScript
@@ -79,15 +81,15 @@ const result = splitText(element, options);
79
81
  | `charClass` | `string` | `"split-char"` | CSS class for character elements |
80
82
  | `wordClass` | `string` | `"split-word"` | CSS class for word elements |
81
83
  | `lineClass` | `string` | `"split-line"` | CSS class for line elements |
82
- | `mask` | `"lines" \| "words" \| "chars"` | — | Wraps elements in `overflow: clip` container for reveal animations |
84
+ | `mask` | `string` | — | Wrap elements in `overflow: clip` container: `"chars"`, `"words"`, or `"lines"` |
83
85
  | `autoSplit` | `boolean` | `false` | Re-split on container resize |
84
86
  | `onResize` | `function` | — | Callback after resize re-split |
85
87
  | `onSplit` | `function` | — | Callback after initial split |
86
88
  | `revertOnComplete` | `boolean` | `false` | Auto-revert when animation completes |
87
89
  | `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
88
90
  | `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
89
- | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split |
90
- | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines after split |
91
+ | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or `(el, index) => object` functions |
92
+ | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines after split. Values can be strings or `(el, index) => string` functions |
91
93
 
92
94
  #### Return Value
93
95
 
@@ -110,19 +112,36 @@ import { SplitText } from 'fetta/react';
110
112
 
111
113
  | Prop | Type | Default | Description |
112
114
  |------|------|---------|-------------|
113
- | `children` | `ReactElement` | — | Single element to split |
114
- | `onSplit` | `function` | | Called after text is split |
115
- | `onResize` | `function` | — | Called on autoSplit re-split |
116
- | `options` | `object` | — | Split options (type, classes, mask, propIndex, disableKerning) |
115
+ | `children` | `ReactElement` | — | Single React element to split |
116
+ | `as` | `keyof JSX.IntrinsicElements` | `"div"` | Wrapper element type |
117
+ | `className` | `string` | — | Class name for wrapper element |
118
+ | `style` | `CSSProperties` | — | Additional styles for wrapper element |
119
+ | `ref` | `Ref<HTMLElement>` | — | Ref to container element |
120
+ | `onSplit` | `(result) => void` | — | Called after text is split |
121
+ | `onResize` | `(result) => void` | — | Called on autoSplit re-split |
122
+ | `options` | `SplitOptions` | — | Split options (type, classes, mask, propIndex, disableKerning) |
117
123
  | `autoSplit` | `boolean` | `false` | Re-split on container resize |
118
124
  | `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
119
125
  | `inView` | `boolean \| InViewOptions` | `false` | Enable viewport detection |
120
- | `onInView` | `function` | — | Called when element enters viewport |
121
- | `onLeaveView` | `function` | — | Called when element leaves viewport |
122
- | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines |
123
- | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines |
126
+ | `onInView` | `(result) => void` | — | Called when element enters viewport |
127
+ | `onLeaveView` | `(result) => void` | — | Called when element leaves viewport |
128
+ | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines. Values can be objects or `(el, index) => object` functions |
129
+ | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values can be strings or `(el, index) => string` functions |
124
130
  | `resetOnLeave` | `boolean` | `false` | Re-apply initialStyles/initialClasses when leaving viewport |
125
131
 
132
+ #### Callback Signature
133
+
134
+ All callbacks (`onSplit`, `onResize`, `onInView`, `onLeaveView`) receive the same result object:
135
+
136
+ ```ts
137
+ {
138
+ chars: HTMLSpanElement[];
139
+ words: HTMLSpanElement[];
140
+ lines: HTMLSpanElement[];
141
+ revert: () => void;
142
+ }
143
+ ```
144
+
126
145
  #### InView Options
127
146
 
128
147
  ```ts
@@ -135,72 +154,58 @@ import { SplitText } from 'fetta/react';
135
154
 
136
155
  ## Examples
137
156
 
138
- ### Masked Line Reveal
157
+ ### Vanilla JavaScript
139
158
 
140
- ```tsx
141
- <SplitText
142
- options={{ type: 'lines', mask: 'lines' }}
143
- onSplit={({ lines }) => {
144
- animate(lines, { y: ['100%', 0] }, { delay: stagger(0.1) });
145
- }}
146
- >
147
- <p>Each line reveals from below</p>
148
- </SplitText>
149
- ```
159
+ #### Basic
150
160
 
151
- ### Scroll-Triggered Animation
161
+ ```js
162
+ import { splitText } from 'fetta';
163
+ import { animate, stagger } from 'motion';
152
164
 
153
- ```tsx
154
- <SplitText
155
- options={{ type: 'words' }}
156
- initialStyles={{
157
- words: { opacity: '0', transform: 'translateY(20px)' }
158
- }}
159
- inView={{ amount: 0.5 }}
160
- onInView={({ words }) => {
161
- animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
162
- }}
163
- resetOnLeave
164
- >
165
- <p>Animates when scrolled into view</p>
166
- </SplitText>
165
+ const { words } = splitText(document.querySelector('h1'));
166
+
167
+ animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
167
168
  ```
168
169
 
169
- ### Auto-Revert After Animation
170
+ #### Masked Line Reveal
170
171
 
171
- ```tsx
172
- <SplitText
173
- revertOnComplete
174
- onSplit={({ chars }) => {
175
- return animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
176
- }}
177
- >
178
- <h1>Reverts to original HTML after animation</h1>
179
- </SplitText>
172
+ ```js
173
+ splitText(element, {
174
+ type: 'lines',
175
+ mask: 'lines',
176
+ onSplit: ({ lines }) => {
177
+ animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
178
+ }
179
+ });
180
180
  ```
181
181
 
182
- ### Responsive Re-split
182
+ #### With GSAP
183
183
 
184
- ```tsx
185
- <SplitText
186
- autoSplit
187
- onSplit={({ lines }) => animateLines(lines)}
188
- onResize={({ lines }) => animateLines(lines)}
189
- >
190
- <p>Re-animates when container resizes</p>
191
- </SplitText>
184
+ ```js
185
+ import { splitText } from 'fetta';
186
+ import gsap from 'gsap';
187
+
188
+ splitText(element, {
189
+ revertOnComplete: true,
190
+ onSplit: ({ words }) => {
191
+ return gsap.from(words, {
192
+ opacity: 0,
193
+ y: 20,
194
+ stagger: 0.05,
195
+ duration: 0.6,
196
+ });
197
+ }
198
+ });
192
199
  ```
193
200
 
194
- ### CSS-Only Animation with Index Props
201
+ #### CSS-Only with Index Props
195
202
 
196
- ```tsx
197
- <SplitText options={{ type: 'chars', propIndex: true }}>
198
- <h1 className="stagger-fade">Hello</h1>
199
- </SplitText>
203
+ ```js
204
+ splitText(element, { type: 'chars', propIndex: true });
200
205
  ```
201
206
 
202
207
  ```css
203
- .stagger-fade .split-char {
208
+ .split-char {
204
209
  opacity: 0;
205
210
  animation: fade-in 0.5s forwards;
206
211
  animation-delay: calc(var(--char-index) * 0.03s);
@@ -211,38 +216,61 @@ import { SplitText } from 'fetta/react';
211
216
  }
212
217
  ```
213
218
 
214
- ### With GSAP
219
+ ### React
220
+
221
+ #### Basic
215
222
 
216
223
  ```tsx
217
- import { SplitText } from 'fetta/react';
218
- import gsap from 'gsap';
219
-
220
224
  <SplitText
221
- revertOnComplete
222
225
  onSplit={({ words }) => {
223
- return gsap.from(words, {
224
- opacity: 0,
225
- y: 20,
226
- stagger: 0.05,
227
- duration: 0.6,
228
- });
226
+ animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
229
227
  }}
230
228
  >
231
- <h1>Works with GSAP</h1>
229
+ <h1>Hello World</h1>
232
230
  </SplitText>
233
231
  ```
234
232
 
235
- ### Nested HTML Elements
233
+ #### Masked Line Reveal
234
+
235
+ ```tsx
236
+ <SplitText
237
+ options={{ type: 'lines', mask: 'lines' }}
238
+ onSplit={({ lines }) => {
239
+ animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
240
+ }}
241
+ >
242
+ <p>Each line reveals from below</p>
243
+ </SplitText>
244
+ ```
245
+
246
+ #### Scroll-Triggered with InView
247
+
248
+ ```tsx
249
+ <SplitText
250
+ options={{ type: 'words' }}
251
+ initialStyles={{
252
+ words: { opacity: '0', transform: 'translateY(20px)' }
253
+ }}
254
+ inView={{ amount: 0.5 }}
255
+ onInView={({ words }) => {
256
+ animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
257
+ }}
258
+ resetOnLeave
259
+ >
260
+ <p>Animates when scrolled into view</p>
261
+ </SplitText>
262
+ ```
236
263
 
237
- Fetta preserves inline elements like links, emphasis, and other formatting. Attributes (href, class, id, data-*, etc.) are maintained.
264
+ #### Auto-Revert After Animation
238
265
 
239
266
  ```tsx
240
267
  <SplitText
268
+ revertOnComplete
241
269
  onSplit={({ chars }) => {
242
- animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
270
+ return animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
243
271
  }}
244
272
  >
245
- <p>Click <a href="/signup">here</a> to <em>get started</em></p>
273
+ <h1>Reverts to original HTML after animation</h1>
246
274
  </SplitText>
247
275
  ```
248
276
 
@@ -258,11 +286,52 @@ Default classes applied to split elements:
258
286
 
259
287
  Each element also receives a `data-index` attribute with its position.
260
288
 
289
+ ## Font Loading
290
+
291
+ For accurate kerning measurements, fonts must be fully loaded before splitting. When using custom fonts in vanilla JS, wait for `document.fonts.ready`:
292
+
293
+ ```ts
294
+ document.fonts.ready.then(() => {
295
+ const { words } = splitText(element);
296
+ animate(words, { opacity: [0, 1] });
297
+ });
298
+ ```
299
+
300
+ The React component handles this automatically — no additional setup required.
301
+
302
+ ## Accessibility
303
+
304
+ Fetta automatically handles accessibility to ensure split text remains readable by screen readers.
305
+
306
+ **Headings and landmarks** — For elements that support `aria-label` natively (headings, `<section>`, `<nav>`, etc.), Fetta adds `aria-hidden="true"` to each split span and an `aria-label` on the parent:
307
+
308
+ ```html
309
+ <!-- After splitting <h1>Hello World</h1> -->
310
+ <h1 aria-label="Hello World">
311
+ <span class="split-word" aria-hidden="true">Hello</span>
312
+ <span class="split-word" aria-hidden="true">World</span>
313
+ </h1>
314
+ ```
315
+
316
+ **Generic elements and nested content** — For `<span>`, `<div>`, `<p>`, or text containing inline elements like links, Fetta wraps the visual content with `aria-hidden="true"` and adds a screen-reader-only copy that preserves the semantic structure:
317
+
318
+ ```html
319
+ <!-- After splitting <p>Click <a href="/signup">here</a> to start</p> -->
320
+ <p>
321
+ <span aria-hidden="true" data-fetta-visual="true">
322
+ <!-- Split visual content -->
323
+ </span>
324
+ <span class="fetta-sr-only" data-fetta-sr-copy="true">
325
+ Click <a href="/signup">here</a> to start
326
+ </span>
327
+ </p>
328
+ ```
329
+
330
+ Pre-existing `aria-label` attributes are always preserved.
331
+
261
332
  ## Notes
262
333
 
263
- - **Fonts must be loaded** before splitting. The React component waits for `document.fonts.ready` automatically.
264
334
  - **Ligatures are disabled** (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
265
- - **Accessibility**: Automatic screen reader support for both simple text and text with nested elements like links.
266
335
 
267
336
  ## Browser Support
268
337
 
@@ -273,6 +342,8 @@ Requires:
273
342
  - `IntersectionObserver`
274
343
  - `Intl.Segmenter`
275
344
 
345
+ **Safari note** — Kerning compensation works but may be slightly less accurate due to Safari's unique font rendering. Differences are typically imperceptible and vary by font, but if you're using `revert()` and notice a subtle shift in some characters, you can bypass compensation with `disableKerning: true`.
346
+
276
347
  ## License
277
348
 
278
349
  MIT
@@ -122,58 +122,9 @@ function copyKerningStyles(target, styles) {
122
122
  if (value) target.style.setProperty(prop, value);
123
123
  });
124
124
  }
125
- function buildCanvasFontString(styles) {
126
- const fontStyle = styles.fontStyle || "normal";
127
- const fontWeight = styles.fontWeight || "normal";
128
- const fontSize = styles.fontSize || "16px";
129
- const fontFamily = styles.fontFamily || "sans-serif";
130
- return [fontStyle, fontWeight, fontSize, fontFamily].filter(Boolean).join(" ");
131
- }
132
- function applyKerningStylesToCanvas(ctx, styles) {
133
- ctx.font = buildCanvasFontString(styles);
134
- const ctxAny = ctx;
135
- const setIfExists = (prop, value) => {
136
- if (!value || value === "normal") return;
137
- if (prop in ctxAny) ctxAny[prop] = value;
138
- };
139
- setIfExists("fontKerning", styles.getPropertyValue("font-kerning"));
140
- setIfExists("fontVariantLigatures", styles.getPropertyValue("font-variant-ligatures"));
141
- setIfExists("fontFeatureSettings", styles.getPropertyValue("font-feature-settings"));
142
- setIfExists("fontVariationSettings", styles.getPropertyValue("font-variation-settings"));
143
- setIfExists("fontOpticalSizing", styles.getPropertyValue("font-optical-sizing"));
144
- setIfExists("fontSizeAdjust", styles.getPropertyValue("font-size-adjust"));
145
- setIfExists("fontStretch", styles.getPropertyValue("font-stretch"));
146
- setIfExists("fontVariantCaps", styles.getPropertyValue("font-variant-caps"));
147
- setIfExists("fontVariantNumeric", styles.getPropertyValue("font-variant-numeric"));
148
- setIfExists("fontVariantEastAsian", styles.getPropertyValue("font-variant-east-asian"));
149
- setIfExists("fontSynthesis", styles.getPropertyValue("font-synthesis"));
150
- setIfExists("fontSynthesisWeight", styles.getPropertyValue("font-synthesis-weight"));
151
- setIfExists("fontSynthesisStyle", styles.getPropertyValue("font-synthesis-style"));
152
- setIfExists("letterSpacing", styles.getPropertyValue("letter-spacing"));
153
- setIfExists("wordSpacing", styles.getPropertyValue("word-spacing"));
154
- setIfExists("textRendering", styles.getPropertyValue("text-rendering"));
155
- setIfExists("direction", styles.getPropertyValue("direction"));
156
- }
157
125
  function buildKerningStyleKey(styles) {
158
126
  return KERNING_STYLE_PROPS.map((prop) => styles.getPropertyValue(prop)).join("|");
159
127
  }
160
- function shouldUseDomKerning(styles) {
161
- const textTransform = styles.getPropertyValue("text-transform");
162
- if (textTransform && textTransform !== "none") return true;
163
- const fontVariant = styles.getPropertyValue("font-variant");
164
- if (fontVariant && fontVariant !== "normal") return true;
165
- const fontStretch = styles.getPropertyValue("font-stretch");
166
- if (fontStretch && fontStretch !== "normal" && fontStretch !== "100%") return true;
167
- const fontFeatureSettings = styles.getPropertyValue("font-feature-settings");
168
- if (fontFeatureSettings && fontFeatureSettings !== "normal") return true;
169
- const fontVariationSettings = styles.getPropertyValue("font-variation-settings");
170
- if (fontVariationSettings && fontVariationSettings !== "normal") return true;
171
- const fontOpticalSizing = styles.getPropertyValue("font-optical-sizing");
172
- if (fontOpticalSizing && fontOpticalSizing !== "auto") return true;
173
- const fontSizeAdjust = styles.getPropertyValue("font-size-adjust");
174
- if (fontSizeAdjust && fontSizeAdjust !== "none") return true;
175
- return false;
176
- }
177
128
  var isSafariBrowser = null;
178
129
  function isSafari() {
179
130
  if (isSafariBrowser !== null) return isSafariBrowser;
@@ -181,29 +132,6 @@ function isSafari() {
181
132
  isSafariBrowser = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
182
133
  return isSafariBrowser;
183
134
  }
184
- function measureKerningCanvas(styleSource, chars, styles) {
185
- const kerningMap = /* @__PURE__ */ new Map();
186
- if (chars.length < 2) return kerningMap;
187
- const canvas = document.createElement("canvas");
188
- const ctx = canvas.getContext("2d");
189
- if (!ctx) return kerningMap;
190
- const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
191
- applyKerningStylesToCanvas(ctx, computedStyles);
192
- const charWidths = /* @__PURE__ */ new Map();
193
- for (const char of new Set(chars)) {
194
- charWidths.set(char, ctx.measureText(char).width);
195
- }
196
- for (let i = 0; i < chars.length - 1; i++) {
197
- const char1 = chars[i];
198
- const char2 = chars[i + 1];
199
- const pairWidth = ctx.measureText(char1 + char2).width;
200
- const kerning = pairWidth - charWidths.get(char1) - charWidths.get(char2);
201
- if (Math.abs(kerning) > 1e-3) {
202
- kerningMap.set(i + 1, kerning);
203
- }
204
- }
205
- return kerningMap;
206
- }
207
135
  function measureKerningDOM(container, styleSource, chars, styles) {
208
136
  const kerningMap = /* @__PURE__ */ new Map();
209
137
  if (chars.length < 2) return kerningMap;
@@ -242,10 +170,47 @@ function measureKerningDOM(container, styleSource, chars, styles) {
242
170
  container.removeChild(measurer);
243
171
  return kerningMap;
244
172
  }
173
+ function measureKerningRange(container, styleSource, chars, styles) {
174
+ const kerningMap = /* @__PURE__ */ new Map();
175
+ if (chars.length < 2) return kerningMap;
176
+ const measurer = document.createElement("span");
177
+ measurer.style.cssText = "position:absolute;visibility:hidden;white-space:pre;";
178
+ const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
179
+ copyKerningStyles(measurer, computedStyles);
180
+ container.appendChild(measurer);
181
+ const range = document.createRange();
182
+ const measureWidth = () => {
183
+ const textNode = measurer.firstChild;
184
+ if (!textNode) return 0;
185
+ range.selectNodeContents(textNode);
186
+ return range.getBoundingClientRect().width;
187
+ };
188
+ const charWidths = /* @__PURE__ */ new Map();
189
+ for (const char of new Set(chars)) {
190
+ measurer.textContent = char;
191
+ charWidths.set(char, measureWidth());
192
+ }
193
+ for (let i = 0; i < chars.length - 1; i++) {
194
+ const char1 = chars[i];
195
+ const char2 = chars[i + 1];
196
+ measurer.textContent = char1 + char2;
197
+ const kerning = measureWidth() - charWidths.get(char1) - charWidths.get(char2);
198
+ if (Math.abs(kerning) > 1e-3) {
199
+ kerningMap.set(i + 1, kerning);
200
+ }
201
+ }
202
+ range.detach();
203
+ container.removeChild(measurer);
204
+ return kerningMap;
205
+ }
245
206
  function measureKerning(container, styleSource, chars, styles) {
246
207
  if (chars.length < 2) return /* @__PURE__ */ new Map();
208
+ if (!container.isConnected) {
209
+ console.warn("splitText: kerning measurement requires a connected DOM element. Skipping kerning.");
210
+ return /* @__PURE__ */ new Map();
211
+ }
247
212
  const computedStyles = styles != null ? styles : getComputedStyle(styleSource);
248
- return isSafari() || shouldUseDomKerning(computedStyles) ? measureKerningDOM(container, styleSource, chars, computedStyles) : measureKerningCanvas(styleSource, chars, computedStyles);
213
+ return isSafari() ? measureKerningDOM(container, styleSource, chars, computedStyles) : measureKerningRange(container, styleSource, chars, computedStyles);
249
214
  }
250
215
  var srOnlyStylesInjected = false;
251
216
  function injectSrOnlyStyles() {
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { splitText } from './chunk-24TCJGUM.js';
1
+ export { splitText } from './chunk-Y4GCLM4K.js';
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-24TCJGUM.js';
1
+ import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-Y4GCLM4K.js';
2
2
  import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, cloneElement } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetta",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Text splitting library with kerning compensation for animations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -80,11 +80,20 @@
80
80
  "keywords": [
81
81
  "text",
82
82
  "split",
83
+ "split-text",
84
+ "splittext",
85
+ "text-animation",
86
+ "text-effect",
83
87
  "animation",
84
88
  "motion",
85
89
  "kerning",
86
90
  "typography",
87
- "gsap"
91
+ "gsap",
92
+ "javascript",
93
+ "typescript",
94
+ "react",
95
+ "web-animation",
96
+ "accessible"
88
97
  ],
89
98
  "license": "MIT",
90
99
  "author": "dimi",