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