@edwinvakayil/calligraphy 1.2.6 → 1.4.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.
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useMemo, useContext, useRef, useEffect, Children, isValidElement } from 'react';
2
+ import { createContext, useMemo, useContext, useRef, useInsertionEffect, useEffect, Children, isValidElement } from 'react';
3
3
 
4
4
  /******************************************************************************
5
5
  Copyright (c) Microsoft Corporation.
@@ -127,28 +127,250 @@ function preloadFonts(families) {
127
127
 
128
128
  const STYLE_ID = "rts-hero-animations";
129
129
  const CSS = `
130
- @keyframes rts-rise{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:translateY(0)}}
131
- @keyframes rts-clip{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0% 0 0)}}
132
- @keyframes rts-pop{0%{opacity:0;transform:scale(0.75)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
133
- @keyframes rts-blur{from{opacity:0;filter:blur(14px);transform:scale(1.04)}to{opacity:1;filter:blur(0);transform:scale(1)}}
134
- @keyframes rts-flip{from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)}to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)}}
135
- @keyframes rts-swipe{from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
136
- @keyframes rts-bounce{0%{opacity:0;transform:translateY(-60px)}60%{opacity:1;transform:translateY(10px)}80%{transform:translateY(-5px)}100%{transform:translateY(0)}}
137
- @keyframes rts-type{from{width:0}to{width:100%}}
138
- @keyframes rts-blink{50%{border-color:transparent}}
139
- @keyframes rts-word-rise{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
140
- @keyframes rts-letter-in{from{opacity:0;transform:translateX(-16px) rotate(-4deg)}to{opacity:1;transform:none}}
141
-
142
- .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
143
- .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
144
- .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
145
- .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
146
- .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
147
- .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
148
- .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
149
- .rts-typewriter{ overflow: hidden; white-space: nowrap; border-right: 2px solid currentColor; width: 0; animation: rts-type 1.6s steps(22,end) both, rts-blink 0.7s step-end 1.6s 3 }
150
- .rts-word { display: inline-block; opacity: 0; transform: translateY(24px); animation: rts-word-rise 0.7s cubic-bezier(0.16,1,0.3,1) both }
151
- .rts-letter { display: inline-block; opacity: 0; transform: translateX(-16px) rotate(-4deg); animation: rts-letter-in 0.5s cubic-bezier(0.16,1,0.3,1) both }
130
+ /* ─── Easing tokens (reused across all keyframes) ──────────────────────────
131
+ All durations are tuned for perceptual smoothness:
132
+ - spring = cubic-bezier(0.16,1,0.3,1) — soft, natural overshoot
133
+ - snap = cubic-bezier(0.77,0,0.18,1) — fast-in, crisp-out
134
+ - bounce = cubic-bezier(0.34,1.56,0.64,1) elastic overshoot
135
+ - cinema = cubic-bezier(0.4,0,0.2,1) — material easing
136
+ ──────────────────────────────────────────────────────────────────────── */
137
+
138
+ /* ── Batch 1 — originals ─────────────────────────────────────────────────── */
139
+ @keyframes rts-rise {from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:translateY(0)}}
140
+ @keyframes rts-clip {from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0% 0 0)}}
141
+ @keyframes rts-pop {0%{opacity:0;transform:scale(0.75)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
142
+ @keyframes rts-blur {from{opacity:0;filter:blur(14px);transform:scale(1.04)}to{opacity:1;filter:blur(0);transform:scale(1)}}
143
+ @keyframes rts-flip {from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)}to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)}}
144
+ @keyframes rts-swipe {from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
145
+ @keyframes rts-bounce {0%{opacity:0;transform:translateY(-60px)}60%{opacity:1;transform:translateY(10px)}80%{transform:translateY(-5px)}100%{transform:translateY(0)}}
146
+ @keyframes rts-type {from{width:0}to{width:100%}}
147
+ @keyframes rts-blink {50%{border-color:transparent}}
148
+ @keyframes rts-word-rise {from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
149
+ @keyframes rts-letter-in {from{opacity:0;transform:translateX(-16px) rotate(-4deg)}to{opacity:1;transform:none}}
150
+
151
+ /* ── Batch 2 modern ────────────────────────────────────────────────────── */
152
+ @keyframes rts-velvet {from{opacity:0;transform:translate(-12px,20px) skewX(4deg)}to{opacity:1;transform:none}}
153
+ @keyframes rts-curtain {from{clip-path:inset(0 0 100% 0)}to{clip-path:inset(0 0 0% 0)}}
154
+ @keyframes rts-morph {0%{opacity:0;transform:scaleY(0.3) scaleX(1.3) translateY(10px)}60%{opacity:1;transform:scaleY(1.08) scaleX(0.97)}100%{transform:none}}
155
+ @keyframes rts-ground {from{transform:translateY(110%);opacity:0}to{transform:translateY(0);opacity:1}}
156
+ @keyframes rts-cascade {from{opacity:0;transform:translateY(-28px) translateX(10px) rotate(8deg)}to{opacity:1;transform:none}}
157
+ @keyframes rts-spotlight {0%{opacity:0;letter-spacing:0.3em;transform:scaleX(1.15)}100%{opacity:1;letter-spacing:-0.03em;transform:scaleX(1)}}
158
+ @keyframes rts-ink {0%{opacity:0;transform:translateY(6px) scale(0.96)}100%{opacity:1;transform:none}}
159
+ @keyframes rts-hinge {from{opacity:0;transform:perspective(400px) rotateY(-40deg) translateX(-20px)}to{opacity:1;transform:perspective(400px) rotateY(0) translateX(0)}}
160
+ @keyframes rts-stretch {0%{opacity:0;transform:scaleX(0.05)}60%{transform:scaleX(1.04)}100%{opacity:1;transform:none}}
161
+ @keyframes rts-peel {from{clip-path:inset(100% 0 0 0)}to{clip-path:inset(0% 0 0 0)}}
162
+ @keyframes rts-fold {from{opacity:0;transform:rotateZ(-8deg) translateY(18px)}to{opacity:1;transform:none}}
163
+ @keyframes rts-shear {from{opacity:0;transform:skewY(8deg) translateY(12px)}to{opacity:1;transform:none}}
164
+ /* ripple — elastic scale from compressed point, outward wave delay */
165
+ @keyframes rts-ripple-w {
166
+ 0% {opacity:0;transform:scale(0.4) translateY(20px)}
167
+ 55% {opacity:1;transform:scale(1.08) translateY(-3px)}
168
+ 75% {transform:scale(0.97) translateY(1px)}
169
+ 100%{opacity:1;transform:scale(1) translateY(0)}
170
+ }
171
+ /* cinch — character pinches on scaleX then snaps open with skew */
172
+ @keyframes rts-cinch {
173
+ 0% {opacity:0;transform:scaleX(0) skewX(20deg)}
174
+ 50% {opacity:1;transform:scaleX(1.12) skewX(-4deg)}
175
+ 75% {transform:scaleX(0.95) skewX(1deg)}
176
+ 100%{opacity:1;transform:scaleX(1) skewX(0)}
177
+ }
178
+ /* tiltrise — words rise while untilting from a sideways lean */
179
+ @keyframes rts-tiltrise {
180
+ 0% {opacity:0;transform:translateY(36px) rotate(-6deg) skewX(8deg)}
181
+ 65% {opacity:1;transform:translateY(-3px) rotate(0.5deg) skewX(-1deg)}
182
+ 100%{opacity:1;transform:translateY(0) rotate(0) skewX(0)}
183
+ }
184
+ @keyframes rts-cardflip {from{opacity:0;transform:perspective(300px) rotateX(-90deg)}to{opacity:1;transform:perspective(300px) rotateX(0)}}
185
+ @keyframes rts-conv-l {from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:translateX(0)}}
186
+ @keyframes rts-conv-r {from{opacity:0;transform:translateX( 40px)}to{opacity:1;transform:translateX(0)}}
187
+ @keyframes rts-splitrise-t{from{opacity:0;transform:translateY(-24px)}to{opacity:1;transform:translateY(0)}}
188
+ @keyframes rts-splitrise-b{from{opacity:0;transform:translateY( 24px)}to{opacity:1;transform:translateY(0)}}
189
+
190
+ /* ── Batch 3 — new mechanics ─────────────────────────────────────────────── */
191
+
192
+ /* tectonic: words slam from alternating sides with skew — unique X+skew combo */
193
+ @keyframes rts-tec-l{from{opacity:0;transform:translateX(-55px) skewX(-7deg)}to{opacity:1;transform:none}}
194
+ @keyframes rts-tec-r{from{opacity:0;transform:translateX( 55px) skewX( 7deg)}to{opacity:1;transform:none}}
195
+
196
+ /* stratify: each word enters from deep Z with blur — different from blur (which is whole-el) */
197
+ @keyframes rts-strat{
198
+ from{opacity:0;transform:perspective(900px) translateZ(-200px) rotateX(10deg);filter:blur(6px)}
199
+ to {opacity:1;transform:perspective(900px) translateZ(0) rotateX(0); filter:blur(0)}
200
+ }
201
+
202
+ /* unfurl: line expands from center — horizontal center-clip, not edge-clip */
203
+ @keyframes rts-unfurl{from{clip-path:inset(0 50% 0 50%)}to{clip-path:inset(0 0% 0 0%)}}
204
+
205
+ /* gravityWell: chars fall from sky with precise physics micro-rotation */
206
+ @keyframes rts-gwell{
207
+ 0% {opacity:0;transform:translateY(-72px) scale(1.18) rotate(-3.5deg)}
208
+ 52% {opacity:1;transform:translateY(7px) scale(0.98) rotate(0.4deg)}
209
+ 78% {transform:translateY(-3px) scale(1.005) rotate(0)}
210
+ 100%{opacity:1;transform:none}
211
+ }
212
+
213
+ /* orbit: words grow from dot, rotating on Z — scale+rotation combo, unique */
214
+ @keyframes rts-orbit{
215
+ from{opacity:0;transform:scale(0.12) rotate(-28deg)}
216
+ 58% {opacity:1;transform:scale(1.07) rotate(2.5deg)}
217
+ 100%{opacity:1;transform:none}
218
+ }
219
+
220
+ /* liquid: per-word squash then overshoot spring — scaleY+scaleX cross-axis */
221
+ @keyframes rts-liquid{
222
+ 0% {opacity:0;transform:scaleY(0.08) scaleX(1.5) translateY(28px)}
223
+ 40% {opacity:1;transform:scaleY(1.12) scaleX(0.94) translateY(-4px)}
224
+ 65% {transform:scaleY(0.97) scaleX(1.015)}
225
+ 100%{opacity:1;transform:none}
226
+ }
227
+
228
+ /* noiseFade: 3 random opacity waveforms per word — signal-lock feel */
229
+ @keyframes rts-nf0{0%{opacity:0}22%{opacity:.7}44%{opacity:.1}66%{opacity:.95}88%{opacity:.4}100%{opacity:1}}
230
+ @keyframes rts-nf1{0%{opacity:0}18%{opacity:.8}38%{opacity:.15}58%{opacity:1}78%{opacity:.5}100%{opacity:1}}
231
+ @keyframes rts-nf2{0%{opacity:0}28%{opacity:.3}50%{opacity:.9}70%{opacity:.05}88%{opacity:.85}100%{opacity:1}}
232
+
233
+ /* slab: scaleX from-left stamp — fastest entrance, print-press energy */
234
+ @keyframes rts-slab{
235
+ 0% {opacity:0;transform:scaleX(0) skewX(8deg)}
236
+ 52% {opacity:1;transform:scaleX(1.05) skewX(-1deg)}
237
+ 100%{opacity:1;transform:none}
238
+ }
239
+
240
+ /* thread: chars ride individual sine-wave Y offsets set via CSS var */
241
+ @keyframes rts-thread{from{opacity:0;transform:translateY(var(--ty,18px))}to{opacity:1;transform:translateY(0)}}
242
+
243
+ /* billboard: whole-line rotateY — different axis from flip (rotateX) */
244
+ @keyframes rts-billboard{
245
+ from{opacity:0;transform:perspective(800px) rotateY(-32deg) translateX(-24px);filter:blur(3px)}
246
+ to {opacity:1;transform:perspective(800px) rotateY(0) translateX(0); filter:blur(0)}
247
+ }
248
+
249
+ /* ═══════════════════════════════════════════════════════════════════════════
250
+ CLASS DECLARATIONS — all animations use animation-fill-mode: both so
251
+ the element stays at its final state without needing JS cleanup.
252
+ ═══════════════════════════════════════════════════════════════════════════ */
253
+
254
+ /* ── Batch 1 whole-element ───────────────────────────────────────────────── */
255
+ .rts-rise {animation:rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both}
256
+ .rts-clip {animation:rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both}
257
+ .rts-pop {animation:rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both}
258
+ .rts-blur {animation:rts-blur 1s cubic-bezier(0.16,1,0.3,1) both}
259
+ .rts-flip {animation:rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both;transform-origin:center bottom}
260
+ .rts-swipe {animation:rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both}
261
+ .rts-bounce {animation:rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both}
262
+ .rts-typewriter {overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;width:0;animation:rts-type 1.6s steps(22,end) both,rts-blink 0.7s step-end 1.6s 3}
263
+
264
+ /* ── Batch 2 whole-element ───────────────────────────────────────────────── */
265
+ .rts-morph {animation:rts-morph 0.8s cubic-bezier(0.34,1.56,0.64,1) both}
266
+ .rts-spotlight {animation:rts-spotlight 1s cubic-bezier(0.16,1,0.3,1) both}
267
+ .rts-stretch {animation:rts-stretch 0.9s cubic-bezier(0.34,1.56,0.64,1) both}
268
+
269
+ /* ── Batch 3 whole-element ───────────────────────────────────────────────── */
270
+ .rts-unfurl {animation:rts-unfurl 0.95s cubic-bezier(0.77,0,0.18,1) both}
271
+ .rts-billboard {animation:rts-billboard 0.95s cubic-bezier(0.16,1,0.3,1) both;transform-origin:left center}
272
+
273
+ /* ── Per-word / per-char spans (batch 1+2) ───────────────────────────────── */
274
+ .rts-word {display:inline-block;opacity:0;transform:translateY(24px);animation:rts-word-rise 0.7s cubic-bezier(0.16,1,0.3,1) both}
275
+ .rts-letter {display:inline-block;opacity:0;transform:translateX(-16px) rotate(-4deg);animation:rts-letter-in 0.5s cubic-bezier(0.16,1,0.3,1) both}
276
+ .rts-velvet-word {display:inline-block;opacity:0;animation:rts-velvet 0.65s cubic-bezier(0.16,1,0.3,1) both}
277
+ .rts-curtain-word {display:inline-block;overflow:hidden;animation:rts-curtain 0.7s cubic-bezier(0.77,0,0.18,1) both}
278
+ .rts-ground-wrap {display:inline-block;overflow:hidden;vertical-align:bottom}
279
+ .rts-ground-inner {display:inline-block;animation:rts-ground 0.65s cubic-bezier(0.16,1,0.3,1) both}
280
+ .rts-cascade-ch {display:inline-block;opacity:0;animation:rts-cascade 0.45s cubic-bezier(0.34,1.56,0.64,1) both}
281
+ .rts-ink-word {display:inline-block;opacity:0;animation:rts-ink 0.9s cubic-bezier(0.16,1,0.3,1) both}
282
+ .rts-hinge-word {display:inline-block;opacity:0;transform-origin:left center;animation:rts-hinge 0.6s cubic-bezier(0.16,1,0.3,1) both}
283
+ .rts-peel-word {display:inline-block;overflow:hidden;animation:rts-peel 0.6s cubic-bezier(0.77,0,0.18,1) both}
284
+ .rts-fold-word {display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-fold 0.6s cubic-bezier(0.34,1.4,0.64,1) both}
285
+ .rts-shear-word {display:inline-block;opacity:0;animation:rts-shear 0.65s cubic-bezier(0.16,1,0.3,1) both}
286
+ .rts-ripple-word {display:inline-block;opacity:0;animation:rts-ripple-w 0.75s cubic-bezier(0.34,1.4,0.64,1) both}
287
+ .rts-cinch-ch {display:inline-block;opacity:0;transform-origin:center;animation:rts-cinch 0.55s cubic-bezier(0.34,1.2,0.64,1) both}
288
+ .rts-tiltrise-word{display:inline-block;opacity:0;animation:rts-tiltrise 0.8s cubic-bezier(0.16,1,0.3,1) both}
289
+ .rts-cardflip-ch {display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-cardflip 0.4s cubic-bezier(0.34,1.4,0.64,1) both}
290
+ .rts-conv-l {display:inline-block;opacity:0;animation:rts-conv-l 0.7s cubic-bezier(0.16,1,0.3,1) both}
291
+ .rts-conv-r {display:inline-block;opacity:0;animation:rts-conv-r 0.7s cubic-bezier(0.16,1,0.3,1) both}
292
+ .rts-splitrise-t {display:inline-block;opacity:0;animation:rts-splitrise-t 0.65s cubic-bezier(0.16,1,0.3,1) both}
293
+ .rts-splitrise-b {display:inline-block;opacity:0;animation:rts-splitrise-b 0.65s cubic-bezier(0.16,1,0.3,1) both}
294
+
295
+ /* ── Per-word / per-char spans (batch 3) ────────────────────────────────── */
296
+ .rts-tec-l {display:inline-block;opacity:0;animation:rts-tec-l 0.65s cubic-bezier(0.16,1,0.3,1) both}
297
+ .rts-tec-r {display:inline-block;opacity:0;animation:rts-tec-r 0.65s cubic-bezier(0.16,1,0.3,1) both}
298
+ .rts-strat-word {display:inline-block;opacity:0;animation:rts-strat 0.85s cubic-bezier(0.16,1,0.3,1) both}
299
+ .rts-gwell-ch {display:inline-block;opacity:0;animation:rts-gwell 0.62s cubic-bezier(0.36,0.07,0.19,0.97) both}
300
+ .rts-orbit-word {display:inline-block;opacity:0;transform-origin:center;animation:rts-orbit 0.65s cubic-bezier(0.34,1.4,0.64,1) both}
301
+ .rts-liquid-word{display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-liquid 0.72s cubic-bezier(0.34,1.56,0.64,1) both}
302
+ .rts-nf0 {display:inline-block;opacity:0;animation:rts-nf0 0.9s ease both}
303
+ .rts-nf1 {display:inline-block;opacity:0;animation:rts-nf1 0.9s ease both}
304
+ .rts-nf2 {display:inline-block;opacity:0;animation:rts-nf2 0.9s ease both}
305
+ .rts-slab-word {display:inline-block;opacity:0;transform-origin:left center;animation:rts-slab 0.5s cubic-bezier(0.77,0,0.18,1) both}
306
+ .rts-thread-ch {display:inline-block;opacity:0;animation:rts-thread 0.55s cubic-bezier(0.16,1,0.3,1) both}
307
+
308
+ /* ── Batch 7 — 8 genuinely new mechanics ────────────────────────────────── */
309
+
310
+ /* glassReveal — backdrop-filter blur evaporates as text solidifies */
311
+ @keyframes rts-glass {
312
+ 0% { opacity:0; filter:blur(0px); backdrop-filter:blur(20px); transform:scale(1.03) }
313
+ 35% { opacity:0.6; filter:blur(2px) }
314
+ 100%{ opacity:1; filter:blur(0px); backdrop-filter:blur(0px); transform:scale(1) }
315
+ }
316
+ .rts-glass { animation:rts-glass 1.2s cubic-bezier(0.16,1,0.3,1) both }
317
+
318
+ /* wordPop — per-word springs from 0 at its own center, pure scale */
319
+ @keyframes rts-wpop {
320
+ 0% { opacity:0; transform:scale(0) }
321
+ 55% { opacity:1; transform:scale(1.08) }
322
+ 75% { transform:scale(0.97) }
323
+ 100%{ transform:scale(1) }
324
+ }
325
+ .rts-wpop-word { display:inline-block; opacity:0; animation:rts-wpop 0.55s cubic-bezier(0.34,1.56,0.64,1) both }
326
+
327
+ /* charDrop — pure gravity fall, no overshoot, no rotation */
328
+ @keyframes rts-cdrop {
329
+ 0% { opacity:0; transform:translateY(-48px) }
330
+ 100%{ opacity:1; transform:translateY(0) }
331
+ }
332
+ .rts-cdrop-ch { display:inline-block; opacity:0; animation:rts-cdrop 0.6s cubic-bezier(0.55,0,1,0.45) both }
333
+
334
+ /* scanline — single-pixel horizontal clip expands to full height */
335
+ @keyframes rts-scan {
336
+ 0% { clip-path:inset(49% 0 49% 0); opacity:0 }
337
+ 15% { opacity:1 }
338
+ 100%{ clip-path:inset(0% 0 0% 0) }
339
+ }
340
+ .rts-scan { animation:rts-scan 0.9s cubic-bezier(0.77,0,0.18,1) both }
341
+
342
+ /* chromaShift — RGB channel offsets collapse to zero */
343
+ @keyframes rts-chroma {
344
+ 0% { opacity:0; text-shadow: -8px 0 0 rgba(255,0,80,0.7), 8px 0 0 rgba(0,200,255,0.7) }
345
+ 40% { opacity:1; text-shadow: -4px 0 0 rgba(255,0,80,0.35), 4px 0 0 rgba(0,200,255,0.35) }
346
+ 100%{ text-shadow:none }
347
+ }
348
+ .rts-chroma { animation:rts-chroma 1s cubic-bezier(0.16,1,0.3,1) both }
349
+
350
+ /* wordFade — pure cross-dissolve with warmth scale, no Y movement */
351
+ @keyframes rts-wfade {
352
+ 0% { opacity:0; transform:scale(0.97) }
353
+ 100%{ opacity:1; transform:scale(1) }
354
+ }
355
+ .rts-wfade-word { display:inline-block; opacity:0; animation:rts-wfade 0.9s cubic-bezier(0.4,0,0.2,1) both }
356
+
357
+ /* rotateIn — full Y-axis card flip per word */
358
+ @keyframes rts-rotatein {
359
+ 0% { opacity:0; transform:perspective(500px) rotateY(90deg) }
360
+ 55% { opacity:1; transform:perspective(500px) rotateY(-8deg) }
361
+ 100%{ transform:perspective(500px) rotateY(0deg) }
362
+ }
363
+ .rts-rotatein-word { display:inline-block; opacity:0; transform-origin:center; animation:rts-rotatein 0.6s cubic-bezier(0.34,1.4,0.64,1) both }
364
+
365
+ /* pressIn — presses down to 0.92 then springs outward past 1 */
366
+ @keyframes rts-pressin {
367
+ 0% { opacity:0; transform:scale(0.5) }
368
+ 30% { opacity:1; transform:scale(0.92) }
369
+ 60% { transform:scale(1.06) }
370
+ 80% { transform:scale(0.98) }
371
+ 100%{ transform:scale(1) }
372
+ }
373
+ .rts-pressin-word { display:inline-block; opacity:0; animation:rts-pressin 0.65s cubic-bezier(0.34,1.56,0.64,1) both }
152
374
  `;
153
375
  function injectAnimationStyles() {
154
376
  if (typeof document === "undefined")
@@ -160,53 +382,48 @@ function injectAnimationStyles() {
160
382
  style.textContent = CSS;
161
383
  document.head.appendChild(style);
162
384
  }
385
+ // ─── Whole-element animation map ─────────────────────────────────────────────
386
+ const WHOLE_CLASS_MAP = {
387
+ rise: "rts-rise",
388
+ clip: "rts-clip",
389
+ pop: "rts-pop",
390
+ blur: "rts-blur",
391
+ flip: "rts-flip",
392
+ swipe: "rts-swipe",
393
+ bounce: "rts-bounce",
394
+ typewriter: "rts-typewriter",
395
+ morph: "rts-morph",
396
+ spotlight: "rts-spotlight",
397
+ stretch: "rts-stretch",
398
+ unfurl: "rts-unfurl",
399
+ billboard: "rts-billboard",
400
+ // Batch 7 — whole-element
401
+ glassReveal: "rts-glass",
402
+ scanline: "rts-scan",
403
+ chromaShift: "rts-chroma",
404
+ };
163
405
  function getAnimationClass(animation) {
164
406
  var _a;
165
- const map = {
166
- rise: "rts-rise",
167
- clip: "rts-clip",
168
- pop: "rts-pop",
169
- blur: "rts-blur",
170
- flip: "rts-flip",
171
- swipe: "rts-swipe",
172
- bounce: "rts-bounce",
173
- typewriter: "rts-typewriter",
174
- stagger: "",
175
- letters: "",
176
- };
177
- return (_a = map[animation]) !== null && _a !== void 0 ? _a : "";
407
+ return (_a = WHOLE_CLASS_MAP[animation]) !== null && _a !== void 0 ? _a : "";
178
408
  }
179
- /**
180
- * Wraps each word in an animated span.
181
- * <em> tokens are preserved as-is in the HTML — Typography's useEffect
182
- * will apply inline styles to them after mount.
183
- */
184
- function buildStaggerHTML(html) {
409
+ function isSplitAnimation(animation) {
410
+ return !(animation in WHOLE_CLASS_MAP);
411
+ }
412
+ // ─── HTML split builders ──────────────────────────────────────────────────────
413
+ function wrapWords(html, cls, step) {
185
414
  var _a;
186
415
  const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
187
- return tokens
188
- .map((tok, i) => {
189
- const delay = (i * 0.07).toFixed(2);
416
+ return tokens.map((tok, i) => {
417
+ const delay = (i * step).toFixed(2);
190
418
  if (tok.startsWith("<em>")) {
191
- // Wrap the inner text in the animated span, keep <em> outside
192
- const inner = tok.slice(4, -5);
193
- return `<em><span class="rts-word" style="animation-delay:${delay}s">${inner}</span></em>`;
419
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
194
420
  }
195
- return `<span class="rts-word" style="animation-delay:${delay}s">${tok}</span>`;
196
- })
197
- .join(" ");
421
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
422
+ }).join(" ");
198
423
  }
199
- /**
200
- * Wraps each character in an animated span.
201
- * <em> tags are preserved in the output — Typography's useEffect applies
202
- * the actual italic/non-italic inline styles after the DOM is ready.
203
- */
204
- function buildLettersHTML(html) {
424
+ function wrapChars(html, cls, step) {
205
425
  const result = [];
206
- let inEm = false;
207
- let delay = 0;
208
- const step = 0.04;
209
- let i = 0;
426
+ let inEm = false, delay = 0, i = 0;
210
427
  while (i < html.length) {
211
428
  if (html.startsWith("<em>", i)) {
212
429
  inEm = true;
@@ -224,14 +441,131 @@ function buildLettersHTML(html) {
224
441
  i++;
225
442
  continue;
226
443
  }
227
- const span = `<span class="rts-letter" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
228
- // Preserve <em> wrapper in DOM — styles applied by useEffect
444
+ const span = `<span class="${cls}" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
229
445
  result.push(inEm ? `<em>${span}</em>` : span);
230
446
  delay += step;
231
447
  i++;
232
448
  }
233
449
  return result.join("");
234
450
  }
451
+ function wrapGround(html, step) {
452
+ var _a;
453
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
454
+ return tokens.map((tok, i) => {
455
+ const delay = (i * step).toFixed(2);
456
+ const inner = tok.startsWith("<em>") ? `<em>${tok.slice(4, -5)}</em>` : tok;
457
+ return `<span class="rts-ground-wrap"><span class="rts-ground-inner" style="animation-delay:${delay}s">${inner}</span></span>`;
458
+ }).join(" ");
459
+ }
460
+ function wrapTectonic(html) {
461
+ var _a;
462
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
463
+ return tokens.map((tok, i) => {
464
+ const cls = i % 2 === 0 ? "rts-tec-l" : "rts-tec-r";
465
+ const delay = (i * 0.09).toFixed(2);
466
+ if (tok.startsWith("<em>")) {
467
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
468
+ }
469
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
470
+ }).join(" ");
471
+ }
472
+ function wrapNoiseFade(html) {
473
+ var _a;
474
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
475
+ return tokens.map((tok, i) => {
476
+ const cls = `rts-nf${i % 3}`;
477
+ const delay = (i * 0.12).toFixed(2);
478
+ if (tok.startsWith("<em>")) {
479
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
480
+ }
481
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
482
+ }).join(" ");
483
+ }
484
+ function wrapConverge(html) {
485
+ var _a;
486
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
487
+ const mid = Math.ceil(tokens.length / 2);
488
+ return tokens.map((tok, i) => {
489
+ const isLeft = i < mid;
490
+ const dist = isLeft ? mid - 1 - i : i - mid;
491
+ const delay = (dist * 0.07).toFixed(2);
492
+ const cls = isLeft ? "rts-conv-l" : "rts-conv-r";
493
+ const inner = tok.startsWith("<em>") ? `<em>${tok.slice(4, -5)}</em>` : tok;
494
+ return `<span class="${cls}" style="animation-delay:${delay}s">${inner}</span>`;
495
+ }).join(" ");
496
+ }
497
+ function wrapRipple(html) {
498
+ var _a;
499
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
500
+ const mid = Math.floor(tokens.length / 2);
501
+ return tokens.map((tok, i) => {
502
+ const delay = (Math.abs(i - mid) * 0.1).toFixed(2);
503
+ if (tok.startsWith("<em>")) {
504
+ return `<em><span class="rts-ripple-word" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
505
+ }
506
+ return `<span class="rts-ripple-word" style="animation-delay:${delay}s">${tok}</span>`;
507
+ }).join(" ");
508
+ }
509
+ function wrapSplitRise(html) {
510
+ var _a;
511
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
512
+ return tokens.map((tok, i) => {
513
+ const cls = i % 2 === 0 ? "rts-splitrise-t" : "rts-splitrise-b";
514
+ const delay = (i * 0.08).toFixed(2);
515
+ if (tok.startsWith("<em>")) {
516
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
517
+ }
518
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
519
+ }).join(" ");
520
+ }
521
+ /**
522
+ * Thread: each char gets a sine-wave Y offset injected as a CSS custom
523
+ * property --ty so the keyframe can read it. Must be post-processed by
524
+ * Typography after innerHTML is set — see applyThreadOffsets().
525
+ */
526
+ function wrapThread(html) {
527
+ return wrapChars(html, "rts-thread-ch", 0.04);
528
+ }
529
+ // ─── Public API ───────────────────────────────────────────────────────────────
530
+ function buildSplitHTML(animation, html) {
531
+ switch (animation) {
532
+ // Batch 1
533
+ case "stagger": return wrapWords(html, "rts-word", 0.07);
534
+ case "letters": return wrapChars(html, "rts-letter", 0.04);
535
+ // Batch 2
536
+ case "velvet": return wrapWords(html, "rts-velvet-word", 0.08);
537
+ case "curtain": return wrapWords(html, "rts-curtain-word", 0.10);
538
+ case "ground": return wrapGround(html, 0.09);
539
+ case "cascade": return wrapChars(html, "rts-cascade-ch", 0.05);
540
+ case "ink": return wrapWords(html, "rts-ink-word", 0.10);
541
+ case "hinge": return wrapWords(html, "rts-hinge-word", 0.09);
542
+ case "peel": return wrapWords(html, "rts-peel-word", 0.10);
543
+ case "fold": return wrapWords(html, "rts-fold-word", 0.09);
544
+ case "shear": return wrapWords(html, "rts-shear-word", 0.08);
545
+ case "ripple": return wrapRipple(html);
546
+ case "cinch": return wrapChars(html, "rts-cinch-ch", 0.048);
547
+ case "tiltrise": return wrapWords(html, "rts-tiltrise-word", 0.09);
548
+ case "cardFlip": return wrapChars(html, "rts-cardflip-ch", 0.045);
549
+ case "converge": return wrapConverge(html);
550
+ case "splitRise": return wrapSplitRise(html);
551
+ // Batch 3
552
+ case "tectonic": return wrapTectonic(html);
553
+ case "stratify": return wrapWords(html, "rts-strat-word", 0.10);
554
+ case "gravityWell": return wrapChars(html, "rts-gwell-ch", 0.05);
555
+ case "orbit": return wrapWords(html, "rts-orbit-word", 0.10);
556
+ case "liquid": return wrapWords(html, "rts-liquid-word", 0.09);
557
+ case "noiseFade": return wrapNoiseFade(html);
558
+ case "slab": return wrapWords(html, "rts-slab-word", 0.11);
559
+ case "thread": return wrapThread(html);
560
+ // Batch 7 — split
561
+ case "wordPop": return wrapWords(html, "rts-wpop-word", 0.07);
562
+ case "charDrop": return wrapChars(html, "rts-cdrop-ch", 0.04);
563
+ case "wordFade": return wrapWords(html, "rts-wfade-word", 0.09);
564
+ case "rotateIn": return wrapWords(html, "rts-rotatein-word", 0.08);
565
+ case "pressIn": return wrapWords(html, "rts-pressin-word", 0.08);
566
+ default: return html;
567
+ }
568
+ }
235
569
 
236
570
  // ─── Defaults ─────────────────────────────────────────────────────────────────
237
571
  const DEFAULT_THEME = {
@@ -396,7 +730,7 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
396
730
  });
397
731
  }
398
732
  function applyEmStylesDOM(container, italic, accentColor, headingFont) {
399
- const applyTo = (el) => {
733
+ const apply = (el) => {
400
734
  if (italic) {
401
735
  el.style.fontFamily = "'Instrument Serif', serif";
402
736
  el.style.fontStyle = "italic";
@@ -410,63 +744,74 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
410
744
  el.style.color = "inherit";
411
745
  }
412
746
  };
413
- container.querySelectorAll("em").forEach(applyTo);
414
- container.querySelectorAll("em > span").forEach(applyTo);
747
+ container.querySelectorAll("em").forEach(apply);
748
+ container.querySelectorAll("em > span").forEach(apply);
415
749
  }
416
750
  // ─── Component ───────────────────────────────────────────────────────────────
417
751
  const Typography = (_a) => {
418
752
  var _b;
419
- var { variant = "Body",
420
- // Explicit undefined = "not set by caller" → fall through to context
421
- font: fontProp, color: colorProp, animation: animationProp, italic: italicProp, accentColor: accentColorProp, align, className, style, children, as, truncate, maxLines } = _a, rest = __rest(_a, ["variant", "font", "color", "animation", "italic", "accentColor", "align", "className", "style", "children", "as", "truncate", "maxLines"]);
753
+ var { variant = "Body", font: fontProp, color: colorProp, animation: animationProp, italic: italicProp, accentColor: accentColorProp, align, className, style, children, as, truncate, maxLines } = _a, rest = __rest(_a, ["variant", "font", "color", "animation", "italic", "accentColor", "align", "className", "style", "children", "as", "truncate", "maxLines"]);
422
754
  const theme = useTypographyTheme();
423
755
  const isHero = variant === "Display" || variant === "H1";
424
756
  const ref = useRef(null);
425
- // Prop wins if explicitly provided; otherwise fall back to theme value.
426
- // We use `?? ` (nullish coalescing) so that false / 0 / "" from props still win.
757
+ // Prop wins; fall back to theme; fall back to built-in default
427
758
  const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
428
759
  const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
429
- const animation = isHero
430
- ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined)
431
- : undefined;
760
+ const animation = isHero ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined) : undefined;
432
761
  const italic = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic;
433
762
  const accentColor = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor;
434
- // Always pre-load Instrument Serif for hero elements
435
- if (isHero) {
436
- injectFont(INSTRUMENT_SERIF_URL);
437
- }
438
- // Inject heading Google Font
439
- if (font && GOOGLE_FONTS.includes(font)) {
440
- injectFont(buildFontUrl(font));
441
- }
442
- // Inject animation keyframes once
443
- if (animation && isHero) {
444
- injectAnimationStyles();
445
- }
446
- // Re-stamp em styles after DOM updates (animation / dangerouslySetInnerHTML path)
763
+ // ── useInsertionEffect: inject <link> and <style> tags ────────────────────
764
+ //
765
+ // WHY useInsertionEffect instead of plain render-phase calls:
766
+ //
767
+ // 1. Server safety — useInsertionEffect (like all effects) is never called
768
+ // on the server, so document.createElement / document.head never run
769
+ // during SSR. The isBrowser guard in ssr.ts is a belt-and-suspenders
770
+ // backup, but the effect boundary is the real guarantee.
771
+ //
772
+ // 2. Correctness React 18 concurrent mode can call the render function
773
+ // multiple times before committing. Doing DOM work in render can fire
774
+ // those side-effects redundantly or out of order. useInsertionEffect
775
+ // fires synchronously before the browser paints, once per commit.
776
+ //
777
+ // 3. No FOUC — because it fires before paint (earlier than useLayoutEffect),
778
+ // the <style> tag is in the DOM before any text is visible, so there is
779
+ // no flash of unstyled / wrong-font text.
780
+ useInsertionEffect(() => {
781
+ // Instrument Serif — always pre-load for hero so toggling italic is instant
782
+ if (isHero) {
783
+ injectFont(INSTRUMENT_SERIF_URL);
784
+ }
785
+ // Heading Google Font
786
+ if (font && GOOGLE_FONTS.includes(font)) {
787
+ injectFont(buildFontUrl(font));
788
+ }
789
+ // Animation keyframe stylesheet
790
+ if (animation && isHero) {
791
+ injectAnimationStyles();
792
+ }
793
+ }, [isHero, font, animation]);
794
+ // ── useEffect: re-stamp inline styles on <em> after DOM updates ───────────
447
795
  useEffect(() => {
448
796
  if (!isHero || !animation || !ref.current)
449
797
  return;
450
798
  applyEmStylesDOM(ref.current, italic, accentColor, font);
451
799
  }, [italic, accentColor, font, animation, isHero]);
452
800
  const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
453
- // ── Animation path ────────────────────────────────────────────────────────
801
+ // ── Animation path: build inner HTML ─────────────────────────────────────
454
802
  let animClass = "";
455
803
  let heroHTML = null;
456
804
  if (animation && isHero) {
457
805
  const rawHTML = childrenToHTML(children);
458
- if (animation === "stagger") {
459
- heroHTML = buildStaggerHTML(rawHTML);
460
- }
461
- else if (animation === "letters") {
462
- heroHTML = buildLettersHTML(rawHTML);
806
+ if (isSplitAnimation(animation)) {
807
+ heroHTML = buildSplitHTML(animation, rawHTML);
463
808
  }
464
809
  else {
465
810
  heroHTML = rawHTML;
466
811
  animClass = getAnimationClass(animation);
467
812
  }
468
813
  }
469
- // ── Computed styles ───────────────────────────────────────────────────────
814
+ // ── Computed container styles ─────────────────────────────────────────────
470
815
  const computedStyle = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, variantStyleMap[variant]), (font ? { fontFamily: `'${font}', sans-serif` } : {})), (color ? { color } : {})), (align ? { textAlign: align } : {})), (truncate
471
816
  ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
472
817
  : {})), (maxLines && !truncate
@@ -481,7 +826,7 @@ const Typography = (_a) => {
481
826
  if (heroHTML !== null) {
482
827
  return (jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
483
828
  }
484
- // ── Render: standard path (real React children) ───────────────────────────
829
+ // ── Render: standard path ────────────────────────────────────────────────
485
830
  const processedChildren = isHero
486
831
  ? renderChildrenWithEmStyles(children, italic, accentColor, font)
487
832
  : children;