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