@edwinvakayil/calligraphy 1.3.0 → 1.5.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,55 +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
- /* ── New modern animations ─────────────────────────────────────────────── */
147
-
148
- @keyframes rts-velvet { from{opacity:0;transform:translate(-12px,20px) skewX(4deg)} to{opacity:1;transform:translate(0,0) skewX(0deg)} }
149
- @keyframes rts-curtain { from{clip-path:inset(0 0 100% 0)} to{clip-path:inset(0 0 0% 0)} }
150
- @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:scaleY(1) scaleX(1)} }
151
- @keyframes rts-ground { from{transform:translateY(110%);opacity:0} to{transform:translateY(0);opacity:1} }
152
- @keyframes rts-cascade { from{opacity:0;transform:translateY(-28px) translateX(10px) rotate(8deg)} to{opacity:1;transform:none} }
153
- @keyframes rts-spotlight { 0%{opacity:0;letter-spacing:0.3em;transform:scaleX(1.15)} 100%{opacity:1;letter-spacing:-0.03em;transform:scaleX(1)} }
154
- @keyframes rts-ink { 0%{opacity:0;transform:translateY(6px) scale(0.96)} 100%{opacity:1;transform:translateY(0) scale(1)} }
155
- @keyframes rts-hinge { from{opacity:0;transform:perspective(400px) rotateY(-40deg) translateX(-20px)} to{opacity:1;transform:perspective(400px) rotateY(0) translateX(0)} }
156
- @keyframes rts-stretch { 0%{opacity:0;transform:scaleX(0.05)} 60%{transform:scaleX(1.04)} 100%{opacity:1;transform:scaleX(1)} }
157
- @keyframes rts-peel { from{clip-path:inset(100% 0 0 0)} to{clip-path:inset(0% 0 0 0)} }
158
-
159
- /* ── Whole-element classes (no splitting needed) ───────────────────────── */
160
- .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
161
- .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
162
- .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
163
- .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
164
- .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
165
- .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
166
- .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
167
- .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 }
168
- .rts-morph { animation: rts-morph 0.8s cubic-bezier(0.34,1.56,0.64,1) both }
169
- .rts-spotlight { animation: rts-spotlight 1s cubic-bezier(0.16,1,0.3,1) both }
170
- .rts-stretch { animation: rts-stretch 0.9s cubic-bezier(0.34,1.56,0.64,1) both }
171
-
172
- /* ── Per-word / per-character span classes ─────────────────────────────── */
173
- .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 }
174
- .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 }
175
- .rts-velvet-word { display:inline-block;opacity:0;animation:rts-velvet 0.65s cubic-bezier(0.16,1,0.3,1) both }
176
- .rts-curtain-word { display:inline-block;overflow:hidden;animation:rts-curtain 0.7s cubic-bezier(0.77,0,0.18,1) both }
177
- .rts-ground-wrap { display:inline-block;overflow:hidden;vertical-align:bottom }
178
- .rts-ground-inner { display:inline-block;animation:rts-ground 0.65s cubic-bezier(0.16,1,0.3,1) both }
179
- .rts-cascade-ch { display:inline-block;opacity:0;animation:rts-cascade 0.45s cubic-bezier(0.34,1.56,0.64,1) both }
180
- .rts-ink-word { display:inline-block;opacity:0;animation:rts-ink 0.9s cubic-bezier(0.16,1,0.3,1) both }
181
- .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 }
182
- .rts-peel-word { display:inline-block;overflow:hidden;animation:rts-peel 0.6s cubic-bezier(0.77,0,0.18,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 }
183
378
  `;
184
379
  function injectAnimationStyles() {
185
380
  if (typeof document === "undefined")
@@ -191,7 +386,7 @@ function injectAnimationStyles() {
191
386
  style.textContent = CSS;
192
387
  document.head.appendChild(style);
193
388
  }
194
- // ─── Whole-element class map ──────────────────────────────────────────────────
389
+ // ─── Whole-element animation map ─────────────────────────────────────────────
195
390
  const WHOLE_CLASS_MAP = {
196
391
  rise: "rts-rise",
197
392
  clip: "rts-clip",
@@ -204,35 +399,35 @@ const WHOLE_CLASS_MAP = {
204
399
  morph: "rts-morph",
205
400
  spotlight: "rts-spotlight",
206
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",
207
408
  };
208
- /** Returns the CSS class for whole-element animations, or "" for split ones. */
209
409
  function getAnimationClass(animation) {
210
410
  var _a;
211
411
  return (_a = WHOLE_CLASS_MAP[animation]) !== null && _a !== void 0 ? _a : "";
212
412
  }
213
- /** True if the animation needs the HTML to be split into word/char spans. */
214
413
  function isSplitAnimation(animation) {
215
414
  return !(animation in WHOLE_CLASS_MAP);
216
415
  }
217
- // ─── HTML builders for split animations ──────────────────────────────────────
218
- function wrapWords(html, cls, delayStep) {
416
+ // ─── HTML split builders ──────────────────────────────────────────────────────
417
+ function wrapWords(html, cls, step) {
219
418
  var _a;
220
419
  const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
221
- return tokens
222
- .map((tok, i) => {
223
- const delay = (i * delayStep).toFixed(2);
420
+ return tokens.map((tok, i) => {
421
+ const delay = (i * step).toFixed(2);
224
422
  if (tok.startsWith("<em>")) {
225
423
  return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
226
424
  }
227
425
  return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
228
- })
229
- .join(" ");
426
+ }).join(" ");
230
427
  }
231
- function wrapChars(html, cls, delayStep) {
428
+ function wrapChars(html, cls, step) {
232
429
  const result = [];
233
- let inEm = false;
234
- let delay = 0;
235
- let i = 0;
430
+ let inEm = false, delay = 0, i = 0;
236
431
  while (i < html.length) {
237
432
  if (html.startsWith("<em>", i)) {
238
433
  inEm = true;
@@ -252,28 +447,107 @@ function wrapChars(html, cls, delayStep) {
252
447
  }
253
448
  const span = `<span class="${cls}" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
254
449
  result.push(inEm ? `<em>${span}</em>` : span);
255
- delay += delayStep;
450
+ delay += step;
256
451
  i++;
257
452
  }
258
453
  return result.join("");
259
454
  }
260
- function wrapGround(html, delayStep) {
455
+ function wrapGround(html, step) {
261
456
  var _a;
262
457
  const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
263
- return tokens
264
- .map((tok, i) => {
265
- const delay = (i * delayStep).toFixed(2);
266
- const inner = tok.startsWith("<em>")
267
- ? `<em>${tok.slice(4, -5)}</em>`
268
- : tok;
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;
269
461
  return `<span class="rts-ground-wrap"><span class="rts-ground-inner" style="animation-delay:${delay}s">${inner}</span></span>`;
270
- })
271
- .join(" ");
462
+ }).join(" ");
272
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
+ /**
534
+ * Called by Typography after dangerouslySetInnerHTML for "thread" animation.
535
+ * Stamps the sine-wave Y offset as --ty on each character span.
536
+ */
537
+ function applyThreadOffsets(container) {
538
+ const spans = container.querySelectorAll(".rts-thread-ch");
539
+ spans.forEach((el, i) => {
540
+ const offset = Math.sin(i * 0.85) * 22;
541
+ el.style.setProperty("--ty", `${offset.toFixed(1)}px`);
542
+ });
543
+ }
544
+ // ─── Public API ───────────────────────────────────────────────────────────────
273
545
  function buildSplitHTML(animation, html) {
274
546
  switch (animation) {
547
+ // Batch 1
275
548
  case "stagger": return wrapWords(html, "rts-word", 0.07);
276
549
  case "letters": return wrapChars(html, "rts-letter", 0.04);
550
+ // Batch 2
277
551
  case "velvet": return wrapWords(html, "rts-velvet-word", 0.08);
278
552
  case "curtain": return wrapWords(html, "rts-curtain-word", 0.10);
279
553
  case "ground": return wrapGround(html, 0.09);
@@ -281,9 +555,114 @@ function buildSplitHTML(animation, html) {
281
555
  case "ink": return wrapWords(html, "rts-ink-word", 0.10);
282
556
  case "hinge": return wrapWords(html, "rts-hinge-word", 0.09);
283
557
  case "peel": return wrapWords(html, "rts-peel-word", 0.10);
558
+ case "fold": return wrapWords(html, "rts-fold-word", 0.09);
559
+ case "shear": return wrapWords(html, "rts-shear-word", 0.08);
560
+ case "ripple": return wrapRipple(html);
561
+ case "cinch": return wrapChars(html, "rts-cinch-ch", 0.048);
562
+ case "tiltrise": return wrapWords(html, "rts-tiltrise-word", 0.09);
563
+ case "cardFlip": return wrapChars(html, "rts-cardflip-ch", 0.045);
564
+ case "converge": return wrapConverge(html);
565
+ case "splitRise": return wrapSplitRise(html);
566
+ // Batch 3
567
+ case "tectonic": return wrapTectonic(html);
568
+ case "stratify": return wrapWords(html, "rts-strat-word", 0.10);
569
+ case "gravityWell": return wrapChars(html, "rts-gwell-ch", 0.05);
570
+ case "orbit": return wrapWords(html, "rts-orbit-word", 0.10);
571
+ case "liquid": return wrapWords(html, "rts-liquid-word", 0.09);
572
+ case "noiseFade": return wrapNoiseFade(html);
573
+ case "slab": return wrapWords(html, "rts-slab-word", 0.11);
574
+ case "thread": return wrapThread(html);
575
+ // Batch 7 — split
576
+ case "wordPop": return wrapWords(html, "rts-wpop-word", 0.07);
577
+ case "charDrop": return wrapChars(html, "rts-cdrop-ch", 0.04);
578
+ case "wordFade": return wrapWords(html, "rts-wfade-word", 0.09);
579
+ case "rotateIn": return wrapWords(html, "rts-rotatein-word", 0.08);
580
+ case "pressIn": return wrapWords(html, "rts-pressin-word", 0.08);
284
581
  default: return html;
285
582
  }
286
583
  }
584
+ // ─── Custom motion builder ────────────────────────────────────────────────────
585
+ /**
586
+ * Injects a one-off @keyframes rule with a generated name and returns that name.
587
+ * Deduped by a hash of the keyframe body so the same keyframe is only injected once.
588
+ */
589
+ function injectCustomKeyframes(keyframeBody) {
590
+ const hash = keyframeBody
591
+ .split("")
592
+ .reduce((acc, ch) => (Math.imul(31, acc) + ch.charCodeAt(0)) | 0, 0)
593
+ .toString(36)
594
+ .replace("-", "n");
595
+ const name = `rts-custom-${hash}`;
596
+ if (typeof document === "undefined")
597
+ return name;
598
+ if (document.getElementById(name))
599
+ return name;
600
+ const style = document.createElement("style");
601
+ style.id = name;
602
+ style.textContent = `@keyframes ${name} { ${keyframeBody} }`;
603
+ document.head.appendChild(style);
604
+ return name;
605
+ }
606
+ /**
607
+ * Builds the innerHTML for a custom motionConfig.
608
+ * - split "none" → returns the raw html unchanged; caller applies class to element
609
+ * - split "words" → wraps each word in a span with the animation + stagger delay
610
+ * - split "chars" → wraps each character in a span with the animation + stagger delay
611
+ *
612
+ * Returns { html, animationValue } where animationValue is the full CSS animation
613
+ * shorthand to set on each span (or on the element itself when split is "none").
614
+ */
615
+ function buildCustomHTML(html, keyframeName, duration, easing, delay, fillMode, split, staggerDelay) {
616
+ var _a;
617
+ const baseAnimation = `${keyframeName} ${duration} ${easing} ${delay} ${fillMode}`;
618
+ if (split === "none") {
619
+ return { html, baseAnimation };
620
+ }
621
+ if (split === "words") {
622
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
623
+ const result = tokens.map((tok, i) => {
624
+ const totalDelay = i === 0
625
+ ? delay
626
+ : `${(parseFloat(delay) + i * staggerDelay).toFixed(3)}s`;
627
+ const anim = `${keyframeName} ${duration} ${easing} ${totalDelay} ${fillMode}`;
628
+ if (tok.startsWith("<em>")) {
629
+ return `<em><span style="display:inline-block;animation:${anim}">${tok.slice(4, -5)}</span></em>`;
630
+ }
631
+ return `<span style="display:inline-block;animation:${anim}">${tok}</span>`;
632
+ });
633
+ return { html: result.join(" "), baseAnimation };
634
+ }
635
+ // chars
636
+ const result = [];
637
+ let inEm = false, charIndex = 0, i = 0;
638
+ while (i < html.length) {
639
+ if (html.startsWith("<em>", i)) {
640
+ inEm = true;
641
+ i += 4;
642
+ continue;
643
+ }
644
+ if (html.startsWith("</em>", i)) {
645
+ inEm = false;
646
+ i += 5;
647
+ continue;
648
+ }
649
+ const ch = html[i];
650
+ if (ch === " ") {
651
+ result.push(" ");
652
+ i++;
653
+ continue;
654
+ }
655
+ const totalDelay = charIndex === 0
656
+ ? delay
657
+ : `${(parseFloat(delay) + charIndex * staggerDelay).toFixed(3)}s`;
658
+ const anim = `${keyframeName} ${duration} ${easing} ${totalDelay} ${fillMode}`;
659
+ const span = `<span style="display:inline-block;animation:${anim}">${ch}</span>`;
660
+ result.push(inEm ? `<em>${span}</em>` : span);
661
+ charIndex++;
662
+ i++;
663
+ }
664
+ return { html: result.join(""), baseAnimation };
665
+ }
287
666
 
288
667
  // ─── Defaults ─────────────────────────────────────────────────────────────────
289
668
  const DEFAULT_THEME = {
@@ -321,7 +700,7 @@ function useTypographyTheme() {
321
700
  return react.useContext(TypographyContext);
322
701
  }
323
702
 
324
- // ─── Static maps ─────────────────────────────────────────────────────────────
703
+ // ─── Variant HTML tag map ───────────────────────────────────────────────────
325
704
  const variantTagMap = {
326
705
  Display: "h1",
327
706
  H1: "h1",
@@ -336,6 +715,7 @@ const variantTagMap = {
336
715
  Label: "label",
337
716
  Caption: "span",
338
717
  };
718
+ // ─── Variant → base CSS styles ────────────────────────────────────────────────
339
719
  const variantStyleMap = {
340
720
  Display: {
341
721
  fontSize: "clamp(2.5rem, 6vw, 5rem)",
@@ -411,9 +791,15 @@ const variantStyleMap = {
411
791
  letterSpacing: "0.03em",
412
792
  },
413
793
  };
414
- // ─── Constants ───────────────────────────────────────────────────────────────
794
+ // ─── Constants ────────────────────────────────────────────────────────────────
795
+ // Always pre-loaded for hero variants so toggling italic is instant with no FOUC
415
796
  const INSTRUMENT_SERIF_URL = "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap";
416
- // ─── Helpers ─────────────────────────────────────────────────────────────────
797
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
798
+ /**
799
+ * Serialise React children to a raw HTML string.
800
+ * Handles plain strings and <em>text</em> elements.
801
+ * Used to feed text into the animation split-builders.
802
+ */
417
803
  function childrenToHTML(children) {
418
804
  var _a, _b;
419
805
  return ((_b = (_a = react.Children.map(children, (child) => {
@@ -421,12 +807,19 @@ function childrenToHTML(children) {
421
807
  return String(child);
422
808
  }
423
809
  if (react.isValidElement(child) && child.type === "em") {
424
- const inner = typeof child.props.children === "string" ? child.props.children : "";
810
+ const inner = typeof child.props.children === "string"
811
+ ? child.props.children
812
+ : "";
425
813
  return `<em>${inner}</em>`;
426
814
  }
427
815
  return "";
428
816
  })) === null || _a === void 0 ? void 0 : _a.join("")) !== null && _b !== void 0 ? _b : "");
429
817
  }
818
+ /**
819
+ * Standard (no-animation) render path.
820
+ * Clones <em> children with explicit inline styles so the font switch is
821
+ * guaranteed — parent fontFamily cannot override a child's own inline style.
822
+ */
430
823
  function renderChildrenWithEmStyles(children, italic, accentColor, headingFont) {
431
824
  const italicStyle = {
432
825
  fontFamily: "'Instrument Serif', serif",
@@ -447,6 +840,11 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
447
840
  return child;
448
841
  });
449
842
  }
843
+ /**
844
+ * Animation (dangerouslySetInnerHTML) render path.
845
+ * After the DOM is written we walk it and stamp inline styles onto every
846
+ * <em> and <em > span — guaranteed to beat any inherited fontFamily.
847
+ */
450
848
  function applyEmStylesDOM(container, italic, accentColor, headingFont) {
451
849
  const apply = (el) => {
452
850
  if (italic) {
@@ -456,7 +854,9 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
456
854
  el.style.color = accentColor;
457
855
  }
458
856
  else {
459
- el.style.fontFamily = headingFont ? `'${headingFont}', sans-serif` : "inherit";
857
+ el.style.fontFamily = headingFont
858
+ ? `'${headingFont}', sans-serif`
859
+ : "inherit";
460
860
  el.style.fontStyle = "normal";
461
861
  el.style.fontWeight = "inherit";
462
862
  el.style.color = "inherit";
@@ -465,61 +865,98 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
465
865
  container.querySelectorAll("em").forEach(apply);
466
866
  container.querySelectorAll("em > span").forEach(apply);
467
867
  }
468
- // ─── Component ───────────────────────────────────────────────────────────────
868
+ // ─── Component ────────────────────────────────────────────────────────────────
469
869
  const Typography = (_a) => {
470
- var _b;
471
- 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"]);
870
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k;
871
+ var { variant = "Body", font: fontProp, color: colorProp, animation: animationProp, motionConfig, motionRef, italic: italicProp, accentColor: accentColorProp, align, className, style, children, as, truncate, maxLines } = _a, rest = __rest(_a, ["variant", "font", "color", "animation", "motionConfig", "motionRef", "italic", "accentColor", "align", "className", "style", "children", "as", "truncate", "maxLines"]);
472
872
  const theme = useTypographyTheme();
473
873
  const isHero = variant === "Display" || variant === "H1";
474
874
  const ref = react.useRef(null);
475
- // Prop wins; fall back to theme; fall back to built-in default
875
+ // Prop wins theme built-in default
476
876
  const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
477
877
  const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
478
- const animation = isHero ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined) : undefined;
479
- const italic = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic;
480
- const accentColor = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor;
481
- // ── useInsertionEffect: inject <link> and <style> tags ────────────────────
482
- //
483
- // WHY useInsertionEffect instead of plain render-phase calls:
484
- //
485
- // 1. Server safety — useInsertionEffect (like all effects) is never called
486
- // on the server, so document.createElement / document.head never run
487
- // during SSR. The isBrowser guard in ssr.ts is a belt-and-suspenders
488
- // backup, but the effect boundary is the real guarantee.
489
- //
490
- // 2. Correctness — React 18 concurrent mode can call the render function
491
- // multiple times before committing. Doing DOM work in render can fire
492
- // those side-effects redundantly or out of order. useInsertionEffect
493
- // fires synchronously before the browser paints, once per commit.
494
- //
495
- // 3. No FOUC — because it fires before paint (earlier than useLayoutEffect),
496
- // the <style> tag is in the DOM before any text is visible, so there is
497
- // no flash of unstyled / wrong-font text.
878
+ const animation = isHero
879
+ ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined)
880
+ : undefined;
881
+ const italic = (_c = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic) !== null && _c !== void 0 ? _c : false;
882
+ const accentColor = (_d = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor) !== null && _d !== void 0 ? _d : "#c8b89a";
883
+ // ── Font & style injection ─────────────────────────────────────────────────
498
884
  react.useInsertionEffect(() => {
499
- // Instrument Serif — always pre-load for hero so toggling italic is instant
500
885
  if (isHero) {
501
886
  injectFont(INSTRUMENT_SERIF_URL);
502
887
  }
503
- // Heading Google Font
504
888
  if (font && GOOGLE_FONTS.includes(font)) {
505
889
  injectFont(buildFontUrl(font));
506
890
  }
507
- // Animation keyframe stylesheet
508
891
  if (animation && isHero) {
509
892
  injectAnimationStyles();
510
893
  }
511
- }, [isHero, font, animation]);
512
- // ── useEffect: re-stamp inline styles on <em> after DOM updates ───────────
894
+ // Inject custom keyframes as soon as the prop arrives
895
+ if (motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes) {
896
+ injectCustomKeyframes(motionConfig.keyframes);
897
+ }
898
+ }, [isHero, font, animation, motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes]);
899
+ // ── Re-stamp <em> inline styles after every relevant change ───────────────
513
900
  react.useEffect(() => {
514
901
  if (!isHero || !animation || !ref.current)
515
902
  return;
516
903
  applyEmStylesDOM(ref.current, italic, accentColor, font);
904
+ if (animation === "thread")
905
+ applyThreadOffsets(ref.current);
517
906
  }, [italic, accentColor, font, animation, isHero]);
907
+ // ── motionRef callback — fires after mount and on every re-render ──────────
908
+ // motionRef wins over animation and motionConfig — the user drives the DOM.
909
+ react.useEffect(() => {
910
+ if (!motionRef)
911
+ return;
912
+ motionRef(ref.current);
913
+ });
914
+ // ── Strip lingering CSS properties after animation ends ───────────────────
915
+ react.useEffect(() => {
916
+ const el = ref.current;
917
+ if (!el || !animation)
918
+ return;
919
+ const cleanup = () => {
920
+ if (animation === "maskSweep") {
921
+ el.style.setProperty("mask-image", "none");
922
+ el.style.setProperty("-webkit-mask-image", "none");
923
+ }
924
+ if (animation === "gradSweep") {
925
+ el.style.animation = "none";
926
+ }
927
+ };
928
+ el.addEventListener("animationend", cleanup, { once: true });
929
+ return () => el.removeEventListener("animationend", cleanup);
930
+ }, [animation]);
518
931
  const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
519
- // ── Animation path: build inner HTML ─────────────────────────────────────
932
+ // ── Build inner HTML — priority: motionRef > motionConfig > animation ──────
520
933
  let animClass = "";
521
934
  let heroHTML = null;
522
- if (animation && isHero) {
935
+ let customAnimStyle;
936
+ // motionRef — no HTML manipulation needed, user handles everything via ref
937
+ if (motionRef) ;
938
+ // motionConfig — works on any variant, not just heroes
939
+ else if (motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes) {
940
+ const keyframeName = injectCustomKeyframes(motionConfig.keyframes);
941
+ const duration = (_e = motionConfig.duration) !== null && _e !== void 0 ? _e : "0.8s";
942
+ const easing = (_f = motionConfig.easing) !== null && _f !== void 0 ? _f : "cubic-bezier(0.16,1,0.3,1)";
943
+ const delay = (_g = motionConfig.delay) !== null && _g !== void 0 ? _g : "0s";
944
+ const fillMode = (_h = motionConfig.fillMode) !== null && _h !== void 0 ? _h : "both";
945
+ const split = (_j = motionConfig.split) !== null && _j !== void 0 ? _j : "none";
946
+ const staggerDelay = (_k = motionConfig.staggerDelay) !== null && _k !== void 0 ? _k : (split === "chars" ? 0.04 : 0.07);
947
+ const rawHTML = childrenToHTML(children);
948
+ const { html, baseAnimation } = buildCustomHTML(rawHTML, keyframeName, duration, easing, delay, fillMode, split, staggerDelay);
949
+ if (split === "none") {
950
+ // Apply animation directly on the element via inline style
951
+ customAnimStyle = baseAnimation;
952
+ heroHTML = rawHTML;
953
+ }
954
+ else {
955
+ heroHTML = html;
956
+ }
957
+ }
958
+ // Built-in animation preset
959
+ else if (animation && isHero) {
523
960
  const rawHTML = childrenToHTML(children);
524
961
  if (isSplitAnimation(animation)) {
525
962
  heroHTML = buildSplitHTML(animation, rawHTML);
@@ -530,8 +967,12 @@ const Typography = (_a) => {
530
967
  }
531
968
  }
532
969
  // ── Computed container styles ─────────────────────────────────────────────
533
- 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
534
- ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
970
+ const computedStyle = Object.assign(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
971
+ ? {
972
+ overflow: "hidden",
973
+ textOverflow: "ellipsis",
974
+ whiteSpace: "nowrap",
975
+ }
535
976
  : {})), (maxLines && !truncate
536
977
  ? {
537
978
  display: "-webkit-box",
@@ -539,12 +980,16 @@ const Typography = (_a) => {
539
980
  WebkitBoxOrient: "vertical",
540
981
  overflow: "hidden",
541
982
  }
542
- : {})), { margin: 0, padding: 0 }), style);
543
- // ── Render: animation path (dangerouslySetInnerHTML) ──────────────────────
983
+ : {})), (customAnimStyle ? { animation: customAnimStyle } : {})), { margin: 0, padding: 0 }), style);
984
+ // ── Render: animation path ────────────────────────────────────────────────
985
+ //
986
+ // key={animation} forces React to unmount + remount the element when the
987
+ // animation value changes, guaranteeing the CSS keyframe fires from frame 0
988
+ // on every switch.
544
989
  if (heroHTML !== null) {
545
990
  return (jsxRuntime.jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
546
991
  }
547
- // ── Render: standard path ────────────────────────────────────────────────
992
+ // ── Render: standard path ─────────────────────────────────────────────────
548
993
  const processedChildren = isHero
549
994
  ? renderChildrenWithEmStyles(children, italic, accentColor, font)
550
995
  : children;
@@ -558,4 +1003,5 @@ exports.buildFontUrl = buildFontUrl;
558
1003
  exports.default = Typography;
559
1004
  exports.injectFont = injectFont;
560
1005
  exports.preloadFonts = preloadFonts;
1006
+ exports.useTypographyTheme = useTypographyTheme;
561
1007
  //# sourceMappingURL=index.js.map