PageSourceSearch

https://reactbits.dev/assets/SplitFlapTextDemo-DUWHsaRI.js

js reactbits.dev collected 2026-10-03 19:25:48 UTC 61,147 bytes, 1,565 lines download raw bytes

1import{r as p,k as e,y as de}from"./index-Bn1bbOAe.js";import{u as he,C as me,T as xe,P as ge,a as be,b as Ce,c as _e}from"./useComponentProps-CkrCaLvB.js";import{C as ye}from"./Customize-DBLruEgy.js";import{P as Z}from"./PreviewColorPickerCustom-CmcW2LtC.js";import{P as J}from"./PreviewSelect-CW4bTxJK.js";import{P as g}from"./PreviewSlider-CJmB9pdC.js";import{P as Te}from"./PreviewSwitch-BKSQdkt2.js";import{D as Re}from"./Dependencies-BritF_2p.js";import"./index-CKdJydTQ.js";import"./backgroundThemeProps-BAOOEp3Q.js";import"./ShikiCode-CUach_VP.js";import"./aiExport--nnywH9U.js";import"./IconSelect-CgfYlisg.js";import"./sparkles-DtoGwhqa.js";import"./maximize-2-DQy3aI7-.js";import"./preview-slider-B9GA5uEa.js";import"./useScrubGesture-BHz_XSAB.js";import"./copy--0PRdOLE.js";const Se=["LAUNCH READY","SYNC ONLINE","SIGNAL LIVE"],O={alpha:"ABCDEFGHIJKLMNOPQRSTUVWXYZ",alphanumeric:"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",numeric:"0123456789"},Y=n=>typeof n=="number"?`${n}px`:n,Ne=n=>O[n]?O[n]:typeof n=="string"&&n.length>0?n:O.alphanumeric,ee=(n,a)=>String(n??"").padEnd(a," ").slice(0,a),j=n=>n.split("").map(a=>({current:a,next:a,flipping:!1,tick:0})),Pe=n=>n.charAt(Math.floor(Math.random()*n.length))||" ",we=(n,a,l)=>{const i=[];for(let d=0;d<a;d+=1)i.push(Pe(l));return i.push(n),i},Ae=()=>{const[n,a]=p.useState(!1);return p.useEffect(()=>{if(typeof window>"u"||!window.matchMedia)return;const l=window.matchMedia("(prefers-reduced-motion: reduce)"),i=()=>a(l.matches);return i(),l.addEventListener("change",i),()=>l.removeEventListener("change",i)},[]),n},Me=({words:n=["LAUNCH READY","SYNC ONLINE","SIGNAL LIVE"],text:a,flipDuration:l=.12,stagger:i=.06,cycleDelay:d=2400,charset:y="alphanumeric",flipsPerChar:T=8,tileColor:P="#111827",textColor:w="#f8fafc",tileRadius:A=8,gap:M=6,fontSize:v=52,loop:R=!0,padTo:S=12,className:E="",style:D={},...k})=>{const L=Ae(),h=p.useRef(null),b=p.useRef(null),t=p.useRef(""),ae=Array.isArray(n)&&n.length>0?n:Se,W=typeof a=="string"?a:ae.map(r=>String(r??"")).join("�"),F=p.useMemo(()=>W.split("�"),[W]),z=p.useMemo(()=>{const r=F.reduce((m,N)=>Math.max(m,N.length),1);return Math.max(1,Math.ceil(Number(S)||0),r)},[S,F]),C=p.useMemo(()=>F.map(r=>ee(r,z)),[F,z]),[H,I]=p.useState(()=>j(C[0]||""));p.useEffect(()=>{const r=()=>{h.current&&(cancelAnimationFrame(h.current),h.current=null),b.current&&(clearTimeout(b.current),b.current=null)};r();const m=C[0]||"";if(t.current=m,I(j(m)),C.length<=1||typeof window>"u")return r;let N=0,q=!1;const G=Math.max(40,(Number(l)||.12)*1e3),le=Math.max(0,(Number(i)||0)*1e3),Q=Math.max(400,Number(d)||2400),ie=Math.max(0,Math.floor(Number(T)||0)),oe=Ne(y),pe=x=>{if(L)return t.current=x,I(j(x)),0;const U=ee(t.current,z),$=x.split("").map((f,c)=>{const o=U[c]||" ";return o===f?null:{index:c,from:o,target:f,sequence:we(f,ie,oe),start:c*le,step:-1,done:!1}}).filter(Boolean);if(!$.length)return t.current=x,I(j(x)),0;const ce=$.reduce((f,c)=>Math.max(f,c.start+c.sequence.length*G),0),fe=performance.now(),ue=f=>{I(c=>{const o=[...c];return f.forEach(u=>{const s=o[u.index];s&&(o[u.index]={current:u.current,next:u.next,flipping:!u.done,tick:s.tick+1})}),o})},K=f=>{if(q)return;const c=f-fe,o=[];let u=!1;$.forEach(s=>{const B=c-s.start;if(B<0){u=!0;return}const _=Math.floor(B/G);_<s.sequence.length?(u=!0,_!==s.step&&(s.step=_,o.push({index:s.index,current:_===0?s.from:s.sequence[_-1],next:s.sequence[_],done:!1}))):s.done||(s.done=!0,o.push({index:s.index,current:s.target,next:s.target,done:!0}))}),o.length>0&&ue(o),u?h.current=requestAnimationFrame(K):(t.current=x,h.current=null)};return h.current=requestAnimationFrame(K),ce},X=x=>{b.current=window.setTimeout(()=>{if(q)return;const U=N+1;if(U>=C.length&&!R)return;N=U%C.length;const V=pe(C[N]);X(Q+V)},x)};return X(Q),()=>{q=!0,r()}},[C,z,R,d,l,i,T,y,L]);const re=H.map(r=>r.current).join("").trimEnd(),se={"--split-flap-tile-color":P,"--split-flap-text-color":w,"--split-flap-radius":Y(A),"--split-flap-gap":Y(M),"--split-flap-font-size":Y(v),"--split-flap-flip-duration":`${Math.max(.04,Number(l)||.12)}s`,...D};return e.jsx("div",{className:`split-flap-text ${E}`.trim(),style:se,role:"text","aria-label":re||void 0,...k,children:H.map((r,m)=>e.jsxs("span",{className:"split-flap-text__tile","aria-hidden":"true",children:[e.jsx("span",{className:"split-flap-text__half split-flap-text__half--top",children:e.jsx("span",{className:"split-flap-text__char",children:r.current===" "?" ":r.current})}),e.jsx("span",{className:"split-flap-text__half split-flap-text__half--bottom",children:e.jsx("span",{className:"split-flap-text__char",children:r.flipping?r.next:r.current})}),r.flipping&&e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"split-flap-text__flap split-flap-text__flap--front",children:e.jsx("span",{className:"split-flap-text__char",children:r.current===" "?" ":r.current})},`front-${m}-${r.tick}`),e.jsx("span",{className:"split-flap-text__flap split-flap-text__flap--back",children:e.jsx("span",{className:"split-flap-text__char",children:r.next===" "?" ":r.next})},`back-${m}-${r.tick}`)]})]},`${m}-${H.length}`))})},ve=`'use client';
2
3import { useEffect, useMemo, useRef, useState } from 'react';
4import './SplitFlapText.css';
5
6const DEFAULT_WORDS = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'];
7
8const CHARSETS = {
9  alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
10  alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
11  numeric: '0123456789'
12};
13
14const toCssUnit = value => (typeof value === 'number' ? \`\${value}px\` : value);
15
16const resolveCharset = charset => {
17  if (CHARSETS[charset]) return CHARSETS[charset];
18  return typeof charset === 'string' && charset.length >
18 0 ? charset : CHARSETS.alphanumeric;
19};
20
21const normalizePhrase = (phrase, width) => {
22  const safe = String(phrase ?? '');
23  return safe.padEnd(width, ' ').slice(0, width);
24};
25
26const createTiles = phrase =>
27  phrase.split('').map(char => ({
28    current: char,
29    next: char,
30    flipping: false,
31    tick: 0
32  }));
33
34const sampleChar = charset => charset.charAt(Math.floor(Math.random() * charset.length)) || ' ';
35
36const buildSequence = (target, flips, charset) => {
37  const steps = [];
38  for (let i = 0; i < flips; i += 1) {
39    steps.push(sampleChar(charset));
40  }
41  steps.push(target);
42  return steps;
43};
44
45const usePrefersReducedMotion = () => {
46  const [prefersReduced, setPrefersReduced] = useState(false);
47
48  useEffect(() => {
49    if (typeof window === 'undefined' || !window.matchMedia) return;
50
51    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
52    const handleChange = () => setPrefersReduced(mediaQuery.matches);
53
54    handleChange();
55    mediaQuery.addEventListener('change', handleChange);
56
57    return () => mediaQuery.removeEventListener('change', handleChange);
58  }, []);
59
60  return prefersReduced;
61};
62
63const SplitFlapText = ({
64  words = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'],
65  text,
66  flipDuration = 0.12,
67  stagger = 0.06,
68  cycleDelay = 2400,
69  charset = 'alphanumeric',
70  flipsPerChar = 8,
71  tileColor = '#111827',
72  textColor = '#f8fafc',
73  tileRadius = 8,
74  gap = 6,
75  fontSize = 52,
76  loop = true,
77  padTo = 12,
78  className = '',
79  style = {},
80  ...props
81}) => {
82  const prefersReducedMotion = usePrefersReducedMotion();
83  const rafRef = useRef(null);
84  const cycleTimerRef = useRef(null);
85  const currentTextRef = useRef('');
86
87  const sourceWords = Array.isArray(words) && words.length > 0 ? words : DEFAULT_WORDS;
88  const phrasesKey = typeof text === 'string' ? text : sourceWords.map(word => String(word ?? '')).join('\\u001f');
89  const phrases = useMemo(() => phrasesKey.split('\\u001f'), [phrasesKey]);
90
91  const width = useMemo(() => {
92    const longest = phrases.reduce((max, phrase) => Math.max(max, phrase.length), 1);
93    return Math.max(1, Math.ceil(Number(padTo) || 0), longest);
94  }, [padTo, phrases]);
95
96  const normalizedPhrases = useMemo(() => phrases.map(phrase => normalizePhrase(phrase, width)), [phrases, width]);
97
98  const [tiles, setTiles] = useState(() => createTiles(normalizedPhrases[0] || ''));
99
100  useEffect(() => {
101    const clearAnimation = () => {
102      if (rafRef.current) {
103        cancelAnimationFrame(rafRef.current);
104        rafRef.current = null;
105      }
106
107      if (cycleTimerRef.current) {
108        clearTimeout(cycleTimerRef.current);
109        cycleTimerRef.current = null;
110      }
111    };
112
113    clearAnimation();
114
115    const firstPhrase = normalizedPhrases[0] || '';
116    currentTextRef.current = firstPhrase;
117    setTiles(createTiles(firstPhrase));
118
119    if (normalizedPhrases.length <= 1 || typeof window === 'undefined') {
120      return clearAnimation;
121    }
122
123    let phraseIndex = 0;
124    let cancelled = false;
125
126    const safeFlipMs = Math.max(40, (Number(flipDuration) || 0.12) * 1000);
127    const safeStaggerMs = Math.max(0, (Number(stagger) || 0) * 1000);
128    const safeCycleDelay = Math.max(400, Number(cycleDelay) || 2400);
129    const safeFlips = Math.max(0, Math.floor(Number(flipsPerChar) || 0));
130    const activeCharset = resolveCharset(charset);
131
132    const animateTo = targetPhrase => {
133      if (prefersReducedMotion) {
134        currentTextRef.current = targetPhrase;
135        setTiles(createTiles(targetPhrase));
136        return 0;
137      }
138
139      const fromPhrase = normalizePhrase(currentTextRef.current, width);
140      const targetChars = targetPhrase.split('');
141
142      const plans = targetChars
143        .map((targetChar, index) => {
144          const fromChar = fromPhrase[index] || ' ';
145          if (fromChar === targetChar) return null;
146
147          return {
148            index,
149            from: fromChar,
150            target: targetChar,
151            sequence: buildSequence(targetChar, safeFlips, activeCharset),
152            start: index * safeStaggerMs,
153            step: -1,
154            done: false
155          };
156        })
157        .filter(Boolean);
158
159      if (!plans.length) {
160        currentTextRef.current = targetPhrase;
161        setTiles(createTiles(targetPhrase));
162        return 0;
163      }
164
165      const totalDuration = plans.reduce(
166        (max, plan) => Math.max(max, plan.start + plan.sequence.length * safeFlipMs),
167        0
168      );
169      const startedAt = performance.now();
170
171      const updateTiles = updates => {
172        setTiles(previous => {
173          const nextTiles = [...previous];
174          updates.forEach(update => {
175            const tile = nextTiles[update.index];
176            if (!tile) return;
177
178            nextTiles[update.index] = {
179              current: update.current,
180              next: update.next,
181              flipping: !update.done,
182              tick: tile.tick + 1
183            };
184          });
185          return nextTiles;
186        });
187      };
188
189      const tick = now => {
190        if (cancelled) return;
191
192        const elapsed = now - startedAt;
193        const updates = [];
194        let shouldContinue = false;
195
196        plans.forEach(plan => {
197          const localElapsed = elapsed - plan.start;
198
199          if (localElapsed < 0) {
200            shouldContinue = true;
201            return;
202          }
203
204          const step = Math.floor(localElapsed / safeFlipMs);
205
206          if (step < plan.sequence.length) {
207            shouldContinue = true;
208
209            if (step !== plan.step) {
210              plan.step = step;
211              updates.push({
212                index: plan.index,
213                current: step === 0 ? plan.from : plan.sequence[step - 1],
214                next: plan.sequence[step],
215                done: false
216              });
217            }
218          } else if (!plan.done) {
219            plan.done = true;
220            updates.push({
221              index: plan.index,
222              current: plan.target,
223              next: plan.target,
224              done: true
225            });
226          }
227        });
228
229        if (updates.length > 0) updateTiles(updates);
230
231        if (shouldContinue) {
232          rafRef.current = requestAnimationFrame(tick);
233        } else {
234          currentTextRef.current = targetPhrase;
235          rafRef.current = null;
236        }
237      };
238
239      rafRef.current = requestAnimationFrame(tick);
240      return totalDuration;
241    };
242
243    const scheduleNext = delay => {
244      cycleTimerRef.current = window.setTimeout(() => {
245        if (cancelled) return;
246
247        const nextIndex = phraseIndex + 1;
248
249        if (nextIndex >= normalizedPhrases.length && !loop) return;
250
251        phraseIndex = nextIndex % normalizedPhrases.length;
252        const animationDuration = animateTo(normalizedPhrases[phraseIndex]);
253        scheduleNext(safeCycleDelay + animationDuration);
254      }, delay);
255    };
256
257    scheduleNext(safeCycleDelay);
258
259    return () => {
260      cancelled = true;
261      clearAnimation();
262    };
263  }, [normalizedPhrases, width, loop, cycleDelay, flipDuration, stagger, flipsPerChar, charset, prefersReducedMotion]);
264
265  const settledText = tiles
266    .map(tile => tile.current)
267    .join('')
268    .trimEnd();
269  const componentStyle = {
270    '--split-flap-tile-color': tileColor,
271    '--split-flap-text-color': textColor,
272    '--split-flap-radius': toCssUnit(tileRadius),
273    '--split-flap-gap': toCssUnit(gap),
274    '--split-flap-font-size': toCssUnit(fontSize),
275    '--split-flap-flip-duration': \`\${Math.max(0.04, Number(flipDuration) || 0.12)}s\`,
276    ...style
277  };
278
279  return (
280    <div
281      className={\`split-flap-text \${className}\`.trim()}
282      style={componentStyle}
283      role="text"
284      aria-label={settledText || undefined}
285      {...props}
286    >
287      {tiles.map((tile, index) => (
288        <span className="split-flap-text__tile" aria-hidden="true" key={\`\${index}-\${tiles.length}\`}>
289          <span className="split-flap-text__half split-flap-text__half--top">
290            <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
291          </span>
292          <span className="split-flap-text__half split-flap-text__half--bottom">
293            <span className="split-flap-text__char">{tile.flipping ? tile.next : tile.current}</span>
294          </span>
295
296          {tile.flipping && (
297            <>
298              <span className="split-flap-text__flap split-flap-text__flap--front" key={\`front-\${index}-\${tile.tick}\`}>
299                <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
300              </span>
301              <span className="split-flap-text__flap split-flap-text__flap--back" key={\`back-\${index}-\${tile.tick}\`}>
302                <span className="split-flap-text__char">{tile.next === ' ' ? '\\u00A0' : tile.next}</span>
303              </span>
304            </>
305          )}
306        </span>
307      ))}
308    </div>
309  );
310};
311
312export default SplitFlapText;
313`,Ee=`.split-flap-text {
314  display: inline-flex;
315  align-items: center;
316  gap: var(--split-flap-gap, 6px);
317  color: var(--split-flap-text-color, #f8fafc);
318  font-family: 'SFMono-Regular', 'Roboto Mono', 'Cascadia Code', 'Liberation Mono', Menlo, monospace;
319  font-size: var(--split-flap-font-size, 52px);
320  font-weight: 760;
321  line-height: 1;
322  letter-spacing: 0.035em;
323  white-space: pre;
324  user-select: none;
325  font-variant-numeric: tabular-nums;
326}
327
328.split-flap-text__tile {
329  position: relative;
330  width: 0.78em;
331  height: 1.08em;
332  overflow: hidden;
333  border-radius: var(--split-flap-radius, 8px);
334  background:
335    radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.16), transparent 44%),
336    linear-gradient(
337      180deg,
338      color-mix(in srgb, var(--split-flap-tile-color, #111827) 82%, white),
339      var(--split-flap-tile-color, #111827)
340    );
341  box-shadow:
342    0 0.035em 0.08em rgba(255, 255, 255, 0.08) inset,
343    0 -0.05em 0.1em rgba(0, 0, 0, 0.38) inset,
344    0 0.16em 0.38em rgba(0, 0, 0, 0.28);
345  perspective: 520px;
346  transform-style: preserve-3d;
347  isolation: isolate;
348}
349
350.split-flap-text__tile::before {
351  content: '';
352  position: absolute;
353  z-index: 8;
354  top: calc(50% - 0.5px);
355  left: 0;
356  width: 100%;
357  height: 1px;
358  background: linear-gradient(
359    90deg,
360    transparent,
361    rgba(255, 255, 255, 0.18) 18%,
362    rgba(0, 0, 0, 0.64) 50%,
363    rgba(255, 255, 255, 0.14) 82%,
364    transparent
365  );
366  box-shadow:
367    0 -1px 0 rgba(255, 255, 255, 0.08),
368    0 1px 0 rgba(0, 0, 0, 0.5);
369  pointer-events: none;
370}
371
372.split-flap-text__tile::after {
373  content: '';
374  position: absolute;
375  inset: 0;
376  z-index: 9;
377  border: 1px solid rgba(255, 255, 255, 0.08);
378  border-radius: inherit;
379  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
380  pointer-events: none;
381}
382
383.split-flap-text__half,
384.split-flap-text__flap {
385  position: absolute;
386  left: 0;
387  width: 100%;
388  height: 50%;
389  overflow: hidden;
390  background:
391    linear-gradient(180deg, rgba(255, 255, 255, 0.07), transparent 34%), var(--split-flap-tile-color, #111827);
392  backface-visibility: hidden;
393}
394
395.split-flap-text__half--top,
396.split-flap-text__flap--front {
397  top: 0;
398}
399
400.split-flap-text__half--bottom,
401.split-flap-text__flap--back {
402  bottom: 0;
403}
404
405.split-flap-text__half--bottom,
406.split-flap-text__flap--back {
407  background:
408    linear-gradient(0deg, rgba(255, 255, 255, 0.06), transparent 38%),
409    color-mix(in srgb, var(--split-flap-tile-color, #111827) 92%, black);
410}
411
412.split-flap-text__char {
413  position: absolute;
414  left: 0;
415  width: 100%;
416  height: 200%;
417  display: flex;
418  align-items: center;
419  justify-content: center;
420  color: var(--split-flap-text-color, #f8fafc);
421  text-shadow:
422    0 0.025em 0 rgba(255, 255, 255, 0.16),
423    0 0.09em 0.16em rgba(0, 0, 0, 0.42);
424}
425
426.split-flap-text__half--top .split-flap-text__char,
427.split-flap-text__flap--front .split-flap-text__char {
428  top: 0;
429}
430
431.split-flap-text__half--bottom .split-flap-text__char,
432.split-flap-text__flap--back .split-flap-text__char {
433  bottom: 0;
434}
435
436.split-flap-text__flap {
437  z-index: 6;
438  will-change: transform, filter;
439  transform-style: preserve-3d;
440}
441
442.split-flap-text__flap--front {
443  transform-origin: center bottom;
444  animation: split-flap-front var(--split-flap-flip-duration, 0.12s) cubic-bezier(0.23, 1, 0.32, 1) both;
445}
446
447.split-flap-text__flap--back {
448  transform-origin: center top;
449  transform: rotateX(90deg);
450  animation: split-flap-back var(--split-flap-flip-duration, 0.12s) cubic-bezier(0.23, 1, 0.32, 1) both;
451}
452
453@keyframes split-flap-front {
454  0% {
455    transform: rotateX(0deg);
456    filter: brightness(1.08);
457  }
458
459  100% {
460    transform: rotateX(-90deg);
461    filter: brightness(0.52);
462  }
463}
464
465@keyframes split-flap-back {
466  0%,
467  45% {
468    transform: rotateX(90deg);
469    filter: brightness(0.58);
470  }
471
472  100% {
473    transform: rotateX(0deg);
474    filter: brightness(1);
475  }
476}
477
478@media (prefers-reduced-motion: reduce) {
479  .split-flap-text__flap {
480    animation: none !important;
481  }
482}
483`,De=`'use client';
484
485import { useEffect, useMemo, useRef, useState } from 'react';
486
487const DEFAULT_WORDS = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'];
488
489const CHARSETS = {
490  alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
491  alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
492  numeric: '0123456789'
493};
494
495const styles = \`
496.split-flap-text{font-family:'SFMono-Regular','Roboto Mono','Cascadia Code','Liberation Mono',Menlo,monospace;font-size:var(--split-flap-font-size,52px);font-weight:760;line-height:1;letter-spacing:.035em;font-variant-numeric:tabular-nums}
497.split-flap-text__tile{position:relative;width:.78em;height:1.08em;overflow:hidden;border-radius:var(--split-flap-radius,8px);background:radial-gradient(circle at 50% 0%,rgba(255,255,255,.16),transparent 44%),linear-gradient(180deg,color-mix(in srgb,var(--split-flap-tile-color,#111827) 82%,white),var(--split-flap-tile-color,#111827));box-shadow:0 .035em .08em rgba(255,255,255,.08) inset,0 -.05em .1em rgba(0,0,0,.38) inset,0 .16em .38em rgba(0,0,0,.28);perspective:520px;transform-style:preserve-3d;isolation:isolate}
498.split-flap-text__tile:before{content:'';position:absolute;z-index:8;top:calc(50% - .5px);left:0;width:100%;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18) 18%,rgba(0,0,0,.64) 50%,rgba(255,255,255,.14) 82%,transparent);box-shadow:0 -1px 0 rgba(255,255,255,.08),0 1px 0 rgba(0,0,0,.5);pointer-events:none}
499.split-flap-text__tile:after{content:'';position:absolute;inset:0;z-index:9;border:1px solid rgba(255,255,255,.08);border-radius:inherit;box-shadow:0 0 0 1px rgba(0,0,0,.2) inset;pointer-events:none}
500.split-flap-text__half,.split-flap-text__flap{position:absolute;left:0;width:100%;height:50%;overflow:hidden;background:linear-gradient(180deg,rgba(255,255,255,.07),transparent 34%),var(--split-flap-tile-color,#111827);backface-visibility:hidden}
501.split-flap-text__half--top,.split-flap-text__flap--front{top:0}
502.split-flap-text__half--bottom,.split-flap-text__flap--back{bottom:0;background:linear-gradient(0deg,rgba(255,255,255,.06),transparent 38%),color-mix(in srgb,var(--split-flap-tile-color,#111827) 92%,black)}
503.split-flap-text__char{position:absolute;left:0;width:100%;height:200%;display:flex;align-items:center;justify-content:center;color:var(--split-flap-text-color,#f8fafc);text-shadow:0 .025em 0 rgba(255,255,255,.16),0 .09em .16em rgba(0,0,0,.42)}
504.split-flap-text__half--top .split-flap-text__char,.split-flap-text__flap--front .split-flap-text__char{top:0}
505.split-flap-text__half--bottom .split-flap-text__char,.split-flap-text__flap--back .split-flap-text__char{bottom:0}
506.split-flap-text__flap{z-index:6;will-change:transform,filter;transform-style:preserve-3d}
507.split-flap-text__flap--front{transform-origin:center bottom;animation:split-flap-front var(--split-flap-flip-duration,.12s) cubic-bezier(.23,1,.32,1) both}
508.split-flap-text__flap--back{transform-origin:center top;transform:rotateX(90deg);animation:split-flap-back var(--split-flap-flip-duration,.12s) cubic-bezier(.23,1,.32,1) both}
509@keyframes split-flap-front{0%{transform:rotateX(0deg);filter:brightness(1.08)}100%{transform:rotateX(-90deg);filter:brightness(.52)}}
510@keyframes split-flap-back{0%,45%{transform:rotateX(90deg);filter:brightness(.58)}100%{transform:rotateX(0deg);filter:brightness(1)}}
511@media (prefers-reduced-motion:reduce){.split-flap-text__flap{animation:none!important}}
512\`;
513
514const toCssUnit = value => (typeof value === 'number' ? \`\${value}px\` : value);
515
516const resolveCharset = charset => {
517  if (CHARSETS[charset]) return CHARSETS[charset];
518  return typeof charset === 'string' && charset.length > 0 ? charset : CHARSETS.alphanumeric;
519};
520
521const normalizePhrase = (phrase, width) => {
522  const safe = String(phrase ?? '');
523  return safe.padEnd(width, ' ').slice(0, width);
524};
525
526const createTiles = phrase =>
527  phrase.split('').map(char => ({
528    current: char,
529    next: char,
530    flipping: false,
531    tick: 0
532  }));
533
534const sampleChar = charset => charset.charAt(Math.floor(Math.random() * charset.length)) || ' ';
535
536const buildSequence = (target, flips, charset) => {
537  const steps = [];
538  for (let i = 0; i < flips; i += 1) {
539    steps.push(sampleChar(charset));
540  }
541  steps.push(target);
542  return steps;
543};
544
545const usePrefersReducedMotion = () => {
546  const [prefersReduced, setPrefersReduced] = useState(false);
547
548  useEffect(() => {
549    if (typeof window === 'undefined' || !window.matchMedia) return;
550
551    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
552    const handleChange = () => setPrefersReduced(mediaQuery.matches);
553
554    handleChange();
555    mediaQuery.addEventListener('change', handleChange);
556
557    return () => mediaQuery.removeEventListener('change', handleChange);
558  }, []);
559
560  return prefersReduced;
561};
562
563const SplitFlapText = ({
564  words = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'],
565  text,
566  flipDuration = 0.12,
567  stagger = 0.06,
568  cycleDelay = 2400,
569  charset = 'alphanumeric',
570  flipsPerChar = 8,
571  tileColor = '#111827',
572  textColor = '#f8fafc',
573  tileRadius = 8,
574  gap = 6,
575  fontSize = 52,
576  loop = true,
577  padTo = 12,
578  className = '',
579  style = {},
580  ...props
581}) => {
582  const prefersReducedMotion = usePrefersReducedMotion();
583  const rafRef = useRef(null);
584  const cycleTimerRef = useRef(null);
585  const currentTextRef = useRef('');
586
587  const sourceWords = Array.isArray(words) && words.length > 0 ? words : DEFAULT_WORDS;
588  const phrasesKey = typeof text === 'string' ? text : sourceWords.map(word => String(word ?? '')).join('\\u001f');
589  const phrases = useMemo(() => phrasesKey.split('\\u001f'), [phrasesKey]);
590
591  const width = useMemo(() => {
592    const longest = phrases.reduce((max, phrase) => Math.max(max, phrase.length), 1);
593    return Math.max(1, Math.ceil(Number(padTo) || 0), longest);
594  }, [padTo, phrases]);
595
596  const normalizedPhrases = useMemo(() => phrases.map(phrase => normalizePhrase(phrase, width)), [phrases, width]);
597
598  const [tiles, setTiles] = useState(() => createTiles(normalizedPhrases[0] || ''));
599
600  useEffect(() => {
601    const clearAnimation = () => {
602      if (rafRef.current) {
603        cancelAnimationFrame(rafRef.current);
604        rafRef.current = null;
605      }
606
607      if (cycleTimerRef.current) {
608        clearTimeout(cycleTimerRef.current);
609        cycleTimerRef.current = null;
610      }
611    };
612
613    clearAnimation();
614
615    const firstPhrase = normalizedPhrases[0] || '';
616    currentTextRef.current = firstPhrase;
617    setTiles(createTiles(firstPhrase));
618
619    if (normalizedPhrases.length <= 1 || typeof window === 'undefined') {
620      return clearAnimation;
621    }
622
623    let phraseIndex = 0;
624    let cancelled = false;
625
626    const safeFlipMs = Math.max(40, (Number(flipDuration) || 0.12) * 1000);
627    const safeStaggerMs = Math.max(0, (Number(stagger) || 0) * 1000);
628    const safeCycleDelay = Math.max(400, Number(cycleDelay) || 2400);
629    const safeFlips = Math.max(0, Math.floor(Number(flipsPerChar) || 0));
630    const activeCharset = resolveCharset(charset);
631
632    const animateTo = targetPhrase => {
633      if (prefersReducedMotion) {
634        currentTextRef.current = targetPhrase;
635        setTiles(createTiles(targetPhrase));
636        return 0;
637      }
638
639      const fromPhrase = normalizePhrase(currentTextRef.current, width);
640      const targetChars = targetPhrase.split('');
641
642      const plans = targetChars
643        .map((targetChar, index) => {
644          const fromChar = fromPhrase[index] || ' ';
645          if (fromChar === targetChar) return null;
646
647          return {
648            index,
649            from: fromChar,
650            target: targetChar,
651            sequence: buildSequence(targetChar, safeFlips, activeCharset),
652            start: index * safeStaggerMs,
653            step: -1,
654            done: false
655          };
656        })
657        .filter(Boolean);
658
659      if (!plans.length) {
660        currentTextRef.current = targetPhrase;
661        setTiles(createTiles(targetPhrase));
662        return 0;
663      }
664
665      const totalDuration = plans.reduce(
666        (max, plan) => Math.max(max, plan.start + plan.sequence.length * safeFlipMs),
667        0
668      );
669      const startedAt = performance.now();
670
671      const updateTiles = updates => {
672        setTiles(previous => {
673          const nextTiles = [...previous];
674          updates.forEach(update => {
675            const tile = nextTiles[update.index];
676            if (!tile) return;
677
678            nextTiles[update.index] = {
679              current: update.current,
680              next: update.next,
681              flipping: !update.done,
682              tick: tile.tick + 1
683            };
684          });
685          return nextTiles;
686        });
687      };
688
689      const tick = now => {
690        if (cancelled) return;
691
692        const elapsed = now - startedAt;
693        const updates = [];
694        let shouldContinue = false;
695
696        plans.forEach(plan => {
697          const localElapsed = elapsed - plan.start;
698
699          if (localElapsed < 0) {
700            shouldContinue = true;
701            return;
702          }
703
704          const step = Math.floor(localElapsed / safeFlipMs);
705
706          if (step < plan.sequence.length) {
707            shouldContinue = true;
708
709            if (step !== plan.step) {
710              plan.step = step;
711              updates.push({
712                index: plan.index,
713                current: step === 0 ? plan.from : plan.sequence[step - 1],
714                next: plan.sequence[step],
715                done: false
716              });
717            }
718          } else if (!plan.done) {
719            plan.done = true;
720            updates.push({
721              index: plan.index,
722              current: plan.target,
723              next: plan.target,
724              done: true
725            });
726          }
727        });
728
729        if (updates.length > 0) updateTiles(updates);
730
731        if (shouldContinue) {
732          rafRef.current = requestAnimationFrame(tick);
733        } else {
734          currentTextRef.current = targetPhrase;
735          rafRef.current = null;
736        }
737      };
738
739      rafRef.current = requestAnimationFrame(tick);
740      return totalDuration;
741    };
742
743    const scheduleNext = delay => {
744      cycleTimerRef.current = window.setTimeout(() => {
745        if (cancelled) return;
746
747        const nextIndex = phraseIndex + 1;
748
749        if (nextIndex >= normalizedPhrases.length && !loop) return;
750
751        phraseIndex = nextIndex % normalizedPhrases.length;
752        const animationDuration = animateTo(normalizedPhrases[phraseIndex]);
753        scheduleNext(safeCycleDelay + animationDuration);
754      }, delay);
755    };
756
757    scheduleNext(safeCycleDelay);
758
759    return () => {
760      cancelled = true;
761      clearAnimation();
762    };
763  }, [normalizedPhrases, width, loop, cycleDelay, flipDuration, stagger, flipsPerChar, charset, prefersReducedMotion]);
764
765  const settledText = tiles
766    .map(tile => tile.current)
767    .join('')
768    .trimEnd();
769  const componentStyle = {
770    '--split-flap-tile-color': tileColor,
771    '--split-flap-text-color': textColor,
772    '--split-flap-radius': toCssUnit(tileRadius),
773    '--split-flap-gap': toCssUnit(gap),
774    '--split-flap-font-size': toCssUnit(fontSize),
775    '--split-flap-flip-duration': \`\${Math.max(0.04, Number(flipDuration) || 0.12)}s\`,
776    ...style
777  };
778
779  return (
780    <>
781      <style>{styles}</style>
782      <div
783        className={\`split-flap-text inline-flex items-center whitespace-pre select-none \${className}\`.trim()}
784        style={componentStyle}
785        role="text"
786        aria-label={settledText || undefined}
787        {...props}
788      >
789        {tiles.map((tile, index) => (
790          <span className="split-flap-text__tile" aria-hidden="true" key={\`\${index}-\${tiles.length}\`}>
791            <span className="split-flap-text__half split-flap-text__half--top">
792              <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
793            </span>
794            <span className="split-flap-text__half split-flap-text__half--bottom">
795              <span className="split-flap-text__char">{tile.flipping ? tile.next : tile.current}</span>
796            </span>
797
798            {tile.flipping && (
799              <>
800                <span
801                  className="split-flap-text__flap split-flap-text__flap--front"
802                  key={\`front-\${index}-\${tile.tick}\`}
803                >
804                  <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
805                </span>
806                <span className="split-flap-text__flap split-flap-text__flap--back" key={\`back-\${index}-\${tile.tick}\`}>
807                  <span className="split-flap-text__char">{tile.next === ' ' ? '\\u00A0' : tile.next}</span>
808                </span>
809              </>
810            )}
811          </span>
812        ))}
813      </div>
814    </>
815  );
816};
817
818export default SplitFlapText;
819`,ke=`'use client';
820
821import { CSSProperties, HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react';
822import './SplitFlapText.css';
823
824type TileState = {
825  current: string;
826  next: string;
827  flipping: boolean;
828  tick: number;
829};
830
831type AnimationPlan = {
832  index: number;
833  from: string;
834  target: string;
835  sequence: string[];
836  start: number;
837  step: number;
838  done: boolean;
839};
840
841type TileUpdate = {
842  index: number;
843  current: string;
844  next: string;
845  done: boolean;
846};
847
848export interface SplitFlapTextProps extends HTMLAttributes<HTMLDivElement> {
849  words?: string[];
850  text?: string;
851  flipDuration?: number;
852  stagger?: number;
853  cycleDelay?: number;
854  charset?: 'alpha' | 'alphanumeric' | 'numeric' | (string & {});
855  flipsPerChar?: number;
856  tileColor?: string;
857  textColor?: string;
858  tileRadius?: number | string;
859  gap?: number | string;
860  fontSize?: number | string;
861  loop?: boolean;
862  padTo?: number;
863}
864
865const DEFAULT_WORDS = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'];
866
867const CHARSETS: Record<string, string> = {
868  alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
869  alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
870  numeric: '0123456789'
871};
872
873const toCssUnit = (value: number | string) => (typeof value === 'number' ? \`\${value}px\` : value);
874
875const resolveCharset = (charset: SplitFlapTextProps['charset']) => {
876  if (charset && CHARSETS[charset]) return CHARSETS[charset];
877  return typeof charset === 'string' && charset.length > 0 ? charset : CHARSETS.alphanumeric;
878};
879
880const normalizePhrase = (phrase: string, width: number) => {
881  const safe = String(phrase ?? '');
882  return safe.padEnd(width, ' ').slice(0, width);
883};
884
885const createTiles = (phrase: string): TileState[] =>
886  phrase.split('').map(char => ({
887    current: char,
888    next: char,
889    flipping: false,
890    tick: 0
891  }));
892
893const sampleChar = (charset: string) => charset.charAt(Math.floor(Math.random() * charset.length)) || ' ';
894
895const buildSequence = (target: string, flips: number, charset: string) => {
896  const steps: string[] = [];
897  for (let i = 0; i < flips; i += 1) {
898    steps.push(sampleChar(charset));
899  }
900  steps.push(target);
901  return steps;
902};
903
904const usePrefersReducedMotion = () => {
905  const [prefersReduced, setPrefersReduced] = useState(false);
906
907  useEffect(() => {
908    if (typeof window === 'undefined' || !window.matchMedia) return;
909
910    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
911    const handleChange = () => setPrefersReduced(mediaQuery.matches);
912
913    handleChange();
914    mediaQuery.addEventListener('change', handleChange);
915
916    return () => mediaQuery.removeEventListener('change', handleChange);
917  }, []);
918
919  return prefersReduced;
920};
921
922const SplitFlapText = ({
923  words = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'],
924  text,
925  flipDuration = 0.12,
926  stagger = 0.06,
927  cycleDelay = 2400,
928  charset = 'alphanumeric',
929  flipsPerChar = 8,
930  tileColor = '#111827',
931  textColor = '#f8fafc',
932  tileRadius = 8,
933  gap = 6,
934  fontSize = 52,
935  loop = true,
936  padTo = 12,
937  className = '',
938  style = {},
939  ...props
940}: SplitFlapTextProps) => {
941  const prefersReducedMotion = usePrefersReducedMotion();
942  const rafRef = useRef<number | null>(null);
943  const cycleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
944  const currentTextRef = useRef('');
945
946  const sourceWords = Array.isArray(words) && words.length > 0 ? words : DEFAULT_WORDS;
947  const phrasesKey = typeof text === 'string' ? text : sourceWords.map(word => String(word ?? '')).join('\\u001f');
948  const phrases = useMemo(() => phrasesKey.split('\\u001f'), [phrasesKey]);
949
950  const width = useMemo(() => {
951    const longest = phrases.reduce((max, phrase) => Math.max(max, phrase.length), 1);
952    return Math.max(1, Math.ceil(Number(padTo) || 0), longest);
953  }, [padTo, phrases]);
954
955  const normalizedPhrases = useMemo(() => phrases.map(phrase => normalizePhrase(phrase, width)), [phrases, width]);
956
957  const [tiles, setTiles] = useState<TileState[]>(() => createTiles(normalizedPhrases[0] || ''));
958
959  useEffect(() => {
960    const clearAnimation = () => {
961      if (rafRef.current) {
962        cancelAnimationFrame(rafRef.current);
963        rafRef.current = null;
964      }
965
966      if (cycleTimerRef.current) {
967        clearTimeout(cycleTimerRef.current);
968        cycleTimerRef.current = null;
969      }
970    };
971
972    clearAnimation();
973
974    const firstPhrase = normalizedPhrases[0] || '';
975    currentTextRef.current = firstPhrase;
976    setTiles(createTiles(firstPhrase));
977
978    if (normalizedPhrases.length <= 1 || typeof window === 'undefined') {
979      return clearAnimation;
980    }
981
982    let phraseIndex = 0;
983    let cancelled = false;
984
985    const safeFlipMs = Math.max(40, (Number(flipDuration) || 0.12) * 1000);
986    const safeStaggerMs = Math.max(0, (Number(stagger) || 0) * 1000);
987    const safeCycleDelay = Math.max(400, Number(cycleDelay) || 2400);
988    const safeFlips = Math.max(0, Math.floor(Number(flipsPerChar) || 0));
989    const activeCharset = resolveCharset(charset);
990
991    const animateTo = (targetPhrase: string) => {
992      if (prefersReducedMotion) {
993        currentTextRef.current = targetPhrase;
994        setTiles(createTiles(targetPhrase));
995        return 0;
996      }
997
998      const fromPhrase = normalizePhrase(currentTextRef.current, width);
999      const targetChars = targetPhrase.split('');
1000
1001      const plans = targetChars
1002        .map<AnimationPlan | null>((targetChar, index) => {
1003          const fromChar = fromPhrase[index] || ' ';
1004          if (fromChar === targetChar) return null;
1005
1006          return {
1007            index,
1008            from: fromChar,
1009            target: targetChar,
1010            sequence: buildSequence(targetChar, safeFlips, activeCharset),
1011            start: index * safeStaggerMs,
1012            step: -1,
1013            done: false
1014          };
1015        })
1016        .filter((plan): plan is AnimationPlan => plan !== null);
1017
1018      if (!plans.length) {
1019        currentTextRef.current = targetPhrase;
1020        setTiles(createTiles(targetPhrase));
1021        return 0;
1022      }
1023
1024      const totalDuration = plans.reduce(
1025        (max, plan) => Math.max(max, plan.start + plan.sequence.length * safeFlipMs),
1026        0
1027      );
1028      const startedAt = performance.now();
1029
1030      const updateTiles = (updates: TileUpdate[]) => {
1031        setTiles(previous => {
1032          const nextTiles = [...previous];
1033          updates.forEach(update => {
1034            const tile = nextTiles[update.index];
1035            if (!tile) return;
1036
1037            nextTiles[update.index] = {
1038              current: update.current,
1039              next: update.next,
1040              flipping: !update.done,
1041              tick: tile.tick + 1
1042            };
1043          });
1044          return nextTiles;
1045        });
1046      };
1047
1048      const tick = (now: number) => {
1049        if (cancelled) return;
1050
1051        const elapsed = now - startedAt;
1052        const updates: TileUpdate[] = [];
1053        let shouldContinue = false;
1054
1055        plans.forEach(plan => {
1056          const localElapsed = elapsed - plan.start;
1057
1058          if (localElapsed < 0) {
1059            shouldContinue = true;
1060            return;
1061          }
1062
1063          const step = Math.floor(localElapsed / safeFlipMs);
1064
1065          if (step < plan.sequence.length) {
1066            shouldContinue = true;
1067
1068            if (step !== plan.step) {
1069              plan.step = step;
1070              updates.push({
1071                index: plan.index,
1072                current: step === 0 ? plan.from : plan.sequence[step - 1],
1073                next: plan.sequence[step],
1074                done: false
1075              });
1076            }
1077          } else if (!plan.done) {
1078            plan.done = true;
1079            updates.push({
1080              index: plan.index,
1081              current: plan.target,
1082              next: plan.target,
1083              done: true
1084            });
1085          }
1086        });
1087
1088        if (updates.length > 0) updateTiles(updates);
1089
1090        if (shouldContinue) {
1091          rafRef.current = requestAnimationFrame(tick);
1092        } else {
1093          currentTextRef.current = targetPhrase;
1094          rafRef.current = null;
1095        }
1096      };
1097
1098      rafRef.current = requestAnimationFrame(tick);
1099      return totalDuration;
1100    };
1101
1102    const scheduleNext = (delay: number) => {
1103      cycleTimerRef.current = window.setTimeout(() => {
1104        if (cancelled) return;
1105
1106        const nextIndex = phraseIndex + 1;
1107
1108        if (nextIndex >= normalizedPhrases.length && !loop) return;
1109
1110        phraseIndex = nextIndex % normalizedPhrases.length;
1111        const animationDuration = animateTo(normalizedPhrases[phraseIndex]);
1112        scheduleNext(safeCycleDelay + animationDuration);
1113      }, delay);
1114    };
1115
1116    scheduleNext(safeCycleDelay);
1117
1118    return () => {
1119      cancelled = true;
1120      clearAnimation();
1121    };
1122  }, [normalizedPhrases, width, loop, cycleDelay, flipDuration, stagger, flipsPerChar, charset, prefersReducedMotion]);
1123
1124  const settledText = tiles
1125    .map(tile => tile.current)
1126    .join('')
1127    .trimEnd();
1128  const componentStyle: CSSProperties & Record<string, string | number | undefined> = {
1129    '--split-flap-tile-color': tileColor,
1130    '--split-flap-text-color': textColor,
1131    '--split-flap-radius': toCssUnit(tileRadius),
1132    '--split-flap-gap': toCssUnit(gap),
1133    '--split-flap-font-size': toCssUnit(fontSize),
1134    '--split-flap-flip-duration': \`\${Math.max(0.04, Number(flipDuration) || 0.12)}s\`,
1135    ...style
1136  };
1137
1138  return (
1139    <div
1140      className={\`split-flap-text \${className}\`.trim()}
1141      style={componentStyle}
1142      role="text"
1143      aria-label={settledText || undefined}
1144      {...props}
1145    >
1146      {tiles.map((tile, index) => (
1147        <span className="split-flap-text__tile" aria-hidden="true" key={\`\${index}-\${tiles.length}\`}>
1148          <span className="split-flap-text__half split-flap-text__half--top">
1149            <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
1150          </span>
1151          <span className="split-flap-text__half split-flap-text__half--bottom">
1152            <span className="split-flap-text__char">{tile.flipping ? tile.next : tile.current}</span>
1153          </span>
1154
1155          {tile.flipping && (
1156            <>
1157              <span className="split-flap-text__flap split-flap-text__flap--front" key={\`front-\${index}-\${tile.tick}\`}>
1158                <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
1159              </span>
1160              <span className="split-flap-text__flap split-flap-text__flap--back" key={\`back-\${index}-\${tile.tick}\`}>
1161                <span className="split-flap-text__char">{tile.next === ' ' ? '\\u00A0' : tile.next}</span>
1162              </span>
1163            </>
1164          )}
1165        </span>
1166      ))}
1167    </div>
1168  );
1169};
1170
1171export default SplitFlapText;
1172`,Le=`'use client';
1173
1174import { CSSProperties, HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react';
1175
1176type TileState = {
1177  current: string;
1178  next: string;
1179  flipping: boolean;
1180  tick: number;
1181};
1182
1183type AnimationPlan = {
1184  index: number;
1185  from: string;
1186  target: string;
1187  sequence: string[];
1188  start: number;
1189  step: number;
1190  done: boolean;
1191};
1192
1193type TileUpdate = {
1194  index: number;
1195  current: string;
1196  next: string;
1197  done: boolean;
1198};
1199
1200export interface SplitFlapTextProps extends HTMLAttributes<HTMLDivElement> {
1201  words?: string[];
1202  text?: string;
1203  flipDuration?: number;
1204  stagger?: number;
1205  cycleDelay?: number;
1206  charset?: 'alpha' | 'alphanumeric' | 'numeric' | (string & {});
1207  flipsPerChar?: number;
1208  tileColor?: string;
1209  textColor?: string;
1210  tileRadius?: number | string;
1211  gap?: number | string;
1212  fontSize?: number | string;
1213  loop?: boolean;
1214  padTo?: number;
1215}
1216
1217const DEFAULT_WORDS = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'];
1218
1219const CHARSETS: Record<string, string> = {
1220  alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
1221  alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
1222  numeric: '0123456789'
1223};
1224
1225const styles = \`
1226.split-flap-text{font-family:'SFMono-Regular','Roboto Mono','Cascadia Code','Liberation Mono',Menlo,monospace;font-size:var(--split-flap-font-size,52px);font-weight:760;line-height:1;letter-spacing:.035em;font-variant-numeric:tabular-nums}
1227.split-flap-text__tile{position:relative;width:.78em;height:1.08em;overflow:hidden;border-radius:var(--split-flap-radius,8px);background:radial-gradient(circle at 50% 0%,rgba(255,255,255,.16),transparent 44%),linear-gradient(180deg,color-mix(in srgb,var(--split-flap-tile-color,#111827) 82%,white),var(--split-flap-tile-color,#111827));box-shadow:0 .035em .08em rgba(255,255,255,.08) inset,0 -.05em .1em rgba(0,0,0,.38) inset,0 .16em .38em rgba(0,0,0,.28);perspective:520px;transform-style:preserve-3d;isolation:isolate}
1228.split-flap-text__tile:before{content:'';position:absolute;z-index:8;top:calc(50% - .5px);left:0;width:100%;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18) 18%,rgba(0,0,0,.64) 50%,rgba(255,255,255,.14) 82%,transparent);box-shadow:0 -1px 0 rgba(255,255,255,.08),0 1px 0 rgba(0,0,0,.5);pointer-events:none}
1229.split-flap-text__tile:after{content:'';position:absolute;inset:0;z-index:9;border:1px solid rgba(255,255,255,.08);border-radius:inherit;box-shadow:0 0 0 1px rgba(0,0,0,.2) inset;pointer-events:none}
1230.split-flap-text__half,.split-flap-text__flap{position:absolute;left:0;width:100%;height:50%;overflow:hidden;background:linear-gradient(180deg,rgba(255,255,255,.07),transparent 34%),var(--split-flap-tile-color,#111827);backface-visibility:hidden}
1231.split-flap-text__half--top,.split-flap-text__flap--front{top:0}
1232.split-flap-text__half--bottom,.split-flap-text__flap--back{bottom:0;background:linear-gradient(0deg,rgba(255,255,255,.06),transparent 38%),color-mix(in srgb,var(--split-flap-tile-color,#111827) 92%,black)}
1233.split-flap-text__char{position:absolute;
1233left:0;width:100%;height:200%;display:flex;align-items:center;justify-content:center;color:var(--split-flap-text-color,#f8fafc);text-shadow:0 .025em 0 rgba(255,255,255,.16),0 .09em .16em rgba(0,0,0,.42)}
1234.split-flap-text__half--top .split-flap-text__char,.split-flap-text__flap--front .split-flap-text__char{top:0}
1235.split-flap-text__half--bottom .split-flap-text__char,.split-flap-text__flap--back .split-flap-text__char{bottom:0}
1236.split-flap-text__flap{z-index:6;will-change:transform,filter;transform-style:preserve-3d}
1237.split-flap-text__flap--front{transform-origin:center bottom;animation:split-flap-front var(--split-flap-flip-duration,.12s) cubic-bezier(.23,1,.32,1) both}
1238.split-flap-text__flap--back{transform-origin:center top;transform:rotateX(90deg);animation:split-flap-back var(--split-flap-flip-duration,.12s) cubic-bezier(.23,1,.32,1) both}
1239@keyframes split-flap-front{0%{transform:rotateX(0deg);filter:brightness(1.08)}100%{transform:rotateX(-90deg);filter:brightness(.52)}}
1240@keyframes split-flap-back{0%,45%{transform:rotateX(90deg);filter:brightness(.58)}100%{transform:rotateX(0deg);filter:brightness(1)}}
1241@media (prefers-reduced-motion:reduce){.split-flap-text__flap{animation:none!important}}
1242\`;
1243
1244const toCssUnit = (value: number | string) => (typeof value === 'number' ? \`\${value}px\` : value);
1245
1246const resolveCharset = (charset: SplitFlapTextProps['charset']) => {
1247  if (charset && CHARSETS[charset]) return CHARSETS[charset];
1248  return typeof charset === 'string' && charset.length > 0 ? charset : CHARSETS.alphanumeric;
1249};
1250
1251const normalizePhrase = (phrase: string, width: number) => {
1252  const safe = String(phrase ?? '');
1253  return safe.padEnd(width, ' ').slice(0, width);
1254};
1255
1256const createTiles = (phrase: string): TileState[] =>
1257  phrase.split('').map(char => ({
1258    current: char,
1259    next: char,
1260    flipping: false,
1261    tick: 0
1262  }));
1263
1264const sampleChar = (charset: string) => charset.charAt(Math.floor(Math.random() * charset.length)) || ' ';
1265
1266const buildSequence = (target: string, flips: number, charset: string) => {
1267  const steps: string[] = [];
1268  for (let i = 0; i < flips; i += 1) {
1269    steps.push(sampleChar(charset));
1270  }
1271  steps.push(target);
1272  return steps;
1273};
1274
1275const usePrefersReducedMotion = () => {
1276  const [prefersReduced, setPrefersReduced] = useState(false);
1277
1278  useEffect(() => {
1279    if (typeof window === 'undefined' || !window.matchMedia) return;
1280
1281    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
1282    const handleChange = () => setPrefersReduced(mediaQuery.matches);
1283
1284    handleChange();
1285    mediaQuery.addEventListener('change', handleChange);
1286
1287    return () => mediaQuery.removeEventListener('change', handleChange);
1288  }, []);
1289
1290  return prefersReduced;
1291};
1292
1293const SplitFlapText = ({
1294  words = ['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE'],
1295  text,
1296  flipDuration = 0.12,
1297  stagger = 0.06,
1298  cycleDelay = 2400,
1299  charset = 'alphanumeric',
1300  flipsPerChar = 8,
1301  tileColor = '#111827',
1302  textColor = '#f8fafc',
1303  tileRadius = 8,
1304  gap = 6,
1305  fontSize = 52,
1306  loop = true,
1307  padTo = 12,
1308  className = '',
1309  style = {},
1310  ...props
1311}: SplitFlapTextProps) => {
1312  const prefersReducedMotion = usePrefersReducedMotion();
1313  const rafRef = useRef<number | null>(null);
1314  const cycleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1315  const currentTextRef = useRef('');
1316
1317  const sourceWords = Array.isArray(words) && words.length > 0 ? words : DEFAULT_WORDS;
1318  const phrasesKey = typeof text === 'string' ? text : sourceWords.map(word => String(word ?? '')).join('\\u001f');
1319  const phrases = useMemo(() => phrasesKey.split('\\u001f'), [phrasesKey]);
1320
1321  const width = useMemo(() => {
1322    const longest = phrases.reduce((max, phrase) => Math.max(max, phrase.length), 1);
1323    return Math.max(1, Math.ceil(Number(padTo) || 0), longest);
1324  }, [padTo, phrases]);
1325
1326  const normalizedPhrases = useMemo(() => phrases.map(phrase => normalizePhrase(phrase, width)), [phrases, width]);
1327
1328  const [tiles, setTiles] = useState<TileState[]>(() => createTiles(normalizedPhrases[0] || ''));
1329
1330  useEffect(() => {
1331    const clearAnimation = () => {
1332      if (rafRef.current) {
1333        cancelAnimationFrame(rafRef.current);
1334        rafRef.current = null;
1335      }
1336
1337      if (cycleTimerRef.current) {
1338        clearTimeout(cycleTimerRef.current);
1339        cycleTimerRef.current = null;
1340      }
1341    };
1342
1343    clearAnimation();
1344
1345    const firstPhrase = normalizedPhrases[0] || '';
1346    currentTextRef.current = firstPhrase;
1347    setTiles(createTiles(firstPhrase));
1348
1349    if (normalizedPhrases.length <= 1 || typeof window === 'undefined') {
1350      return clearAnimation;
1351    }
1352
1353    let phraseIndex = 0;
1354    let cancelled = false;
1355
1356    const safeFlipMs = Math.max(40, (Number(flipDuration) || 0.12) * 1000);
1357    const safeStaggerMs = Math.max(0, (Number(stagger) || 0) * 1000);
1358    const safeCycleDelay = Math.max(400, Number(cycleDelay) || 2400);
1359    const safeFlips = Math.max(0, Math.floor(Number(flipsPerChar) || 0));
1360    const activeCharset = resolveCharset(charset);
1361
1362    const animateTo = (targetPhrase: string) => {
1363      if (prefersReducedMotion) {
1364        currentTextRef.current = targetPhrase;
1365        setTiles(createTiles(targetPhrase));
1366        return 0;
1367      }
1368
1369      const fromPhrase = normalizePhrase(currentTextRef.current, width);
1370      const targetChars = targetPhrase.split('');
1371
1372      const plans = targetChars
1373        .map<AnimationPlan | null>((targetChar, index) => {
1374          const fromChar = fromPhrase[index] || ' ';
1375          if (fromChar === targetChar) return null;
1376
1377          return {
1378            index,
1379            from: fromChar,
1380            target: targetChar,
1381            sequence: buildSequence(targetChar, safeFlips, activeCharset),
1382            start: index * safeStaggerMs,
1383            step: -1,
1384            done: false
1385          };
1386        })
1387        .filter((plan): plan is AnimationPlan => plan !== null);
1388
1389      if (!plans.length) {
1390        currentTextRef.current = targetPhrase;
1391        setTiles(createTiles(targetPhrase));
1392        return 0;
1393      }
1394
1395      const totalDuration = plans.reduce(
1396        (max, plan) => Math.max(max, plan.start + plan.sequence.length * safeFlipMs),
1397        0
1398      );
1399      const startedAt = performance.now();
1400
1401      const updateTiles = (updates: TileUpdate[]) => {
1402        setTiles(previous => {
1403          const nextTiles = [...previous];
1404          updates.forEach(update => {
1405            const tile = nextTiles[update.index];
1406            if (!tile) return;
1407
1408            nextTiles[update.index] = {
1409              current: update.current,
1410              next: update.next,
1411              flipping: !update.done,
1412              tick: tile.tick + 1
1413            };
1414          });
1415          return nextTiles;
1416        });
1417      };
1418
1419      const tick = (now: number) => {
1420        if (cancelled) return;
1421
1422        const elapsed = now - startedAt;
1423        const updates: TileUpdate[] = [];
1424        let shouldContinue = false;
1425
1426        plans.forEach(plan => {
1427          const localElapsed = elapsed - plan.start;
1428
1429          if (localElapsed < 0) {
1430            shouldContinue = true;
1431            return;
1432          }
1433
1434          const step = Math.floor(localElapsed / safeFlipMs);
1435
1436          if (step < plan.sequence.length) {
1437            shouldContinue = true;
1438
1439            if (step !== plan.step) {
1440              plan.step = step;
1441              updates.push({
1442                index: plan.index,
1443                current: step === 0 ? plan.from : plan.sequence[step - 1],
1444                next: plan.sequence[step],
1445                done: false
1446              });
1447            }
1448          } else if (!plan.done) {
1449            plan.done = true;
1450            updates.push({
1451              index: plan.index,
1452              current: plan.target,
1453              next: plan.target,
1454              done: true
1455            });
1456          }
1457        });
1458
1459        if (updates.length > 0) updateTiles(updates);
1460
1461        if (shouldContinue) {
1462          rafRef.current = requestAnimationFrame(tick);
1463        } else {
1464          currentTextRef.current = targetPhrase;
1465          rafRef.current = null;
1466        }
1467      };
1468
1469      rafRef.current = requestAnimationFrame(tick);
1470      return totalDuration;
1471    };
1472
1473    const scheduleNext = (delay: number) => {
1474      cycleTimerRef.current = window.setTimeout(() => {
1475        if (cancelled) return;
1476
1477        const nextIndex = phraseIndex + 1;
1478
1479        if (nextIndex >= normalizedPhrases.length && !loop) return;
1480
1481        phraseIndex = nextIndex % normalizedPhrases.length;
1482        const animationDuration = animateTo(normalizedPhrases[phraseIndex]);
1483        scheduleNext(safeCycleDelay + animationDuration);
1484      }, delay);
1485    };
1486
1487    scheduleNext(safeCycleDelay);
1488
1489    return () => {
1490      cancelled = true;
1491      clearAnimation();
1492    };
1493  }, [normalizedPhrases, width, loop, cycleDelay, flipDuration, stagger, flipsPerChar, charset, prefersReducedMotion]);
1494
1495  const settledText = tiles
1496    .map(tile => tile.current)
1497    .join('')
1498    .trimEnd();
1499  const componentStyle: CSSProperties & Record<string, string | number | undefined> = {
1500    '--split-flap-tile-color': tileColor,
1501    '--split-flap-text-color': textColor,
1502    '--split-flap-radius': toCssUnit(tileRadius),
1503    '--split-flap-gap': toCssUnit(gap),
1504    '--split-flap-font-size': toCssUnit(fontSize),
1505    '--split-flap-flip-duration': \`\${Math.max(0.04, Number(flipDuration) || 0.12)}s\`,
1506    ...style
1507  };
1508
1509  return (
1510    <>
1511      <style>{styles}</style>
1512      <div
1513        className={\`split-flap-text inline-flex items-center whitespace-pre select-none \${className}\`.trim()}
1514        style={componentStyle}
1515        role="text"
1516        aria-label={settledText || undefined}
1517        {...props}
1518      >
1519        {tiles.map((tile, index) => (
1520          <span className="split-flap-text__tile" aria-hidden="true" key={\`\${index}-\${tiles.length}\`}>
1521            <span className="split-flap-text__half split-flap-text__half--top">
1522              <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
1523            </span>
1524            <span className="split-flap-text__half split-flap-text__half--bottom">
1525              <span className="split-flap-text__char">{tile.flipping ? tile.next : tile.current}</span>
1526            </span>
1527
1528            {tile.flipping && (
1529              <>
1530                <span
1531                  className="split-flap-text__flap split-flap-text__flap--front"
1532                  key={\`front-\${index}-\${tile.tick}\`}
1533                >
1534                  <span className="split-flap-text__char">{tile.current === ' ' ? '\\u00A0' : tile.current}</span>
1535                </span>
1536                <span className="split-flap-text__flap split-flap-text__flap--back" key={\`back-\${index}-\${tile.tick}\`}>
1537                  <span className="split-flap-text__char">{tile.next === ' ' ? '\\u00A0' : tile.next}</span>
1538                </span>
1539              </>
1540            )}
1541          </span>
1542        ))}
1543      </div>
1544    </>
1545  );
1546};
1547
1548export default SplitFlapText;
1549`,Fe={dependencies:"",usage:`import SplitFlapText from './SplitFlapText';
1550
1551<SplitFlapText
1552  words={['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE']}
1553  flipDuration={0.12}
1554  stagger={0.06}
1555  cycleDelay={2400}
1556  charset="alphanumeric"
1557  flipsPerChar={8}
1558  tileColor="#111827"
1559  textColor="#f8fafc"
1560  tileRadius={8}
1561  gap={6}
1562  fontSize={52}
1563  loop
1564  padTo={12}
1565/>`,code:ve,css:Ee,tailwind:De,tsCode:ke,tsTailwind:Le},te={launch:["LAUNCH READY","SYNC ONLINE","SIGNAL LIVE"],travel:["BOARDING NOW","FINAL CALL","GATE OPEN"],product:["BUILD FASTER","SHIP CLEANER","DELIGHT USERS"],numeric:["FLIGHT 204","GATE 18","SEAT 07A"]},ne={words:["LAUNCH READY","SYNC ONLINE","SIGNAL LIVE"],flipDuration:.12,stagger:.06,cycleDelay:2400,charset:"alphanumeric",flipsPerChar:8,tileColor:"#111827",textColor:"#f8fafc",tileRadius:8,gap:6,fontSize:52,loop:!0,padTo:12},ze=[{value:"launch",label:"Launch"},{value:"travel",label:"Travel"},{value:"product",label:"Product"},{value:"numeric",label:"Numeric"}],Ie=[{value:"alphanumeric",label:"A-Z + 0-9"},{value:"alpha",label:"A-Z"},{value:"numeric",label:"0-9"},{value:"ABCDEF0123456789•",label:"Hex + dot"}],nt=()=>{var b;const{props:n,updateProp:a,resetProps:l,hasChanges:i}=he(ne),{words:d,flipDuration:y,stagger:T,cycleDelay:P,charset:w,flipsPerChar:A,tileColor:M,textColor:v,tileRadius:R,gap:S,fontSize:E,loop:D,padTo:k}=n,L=((b=Object.entries(te).find(([,t])=>t.join("�")===d.join("�")))==null?void 0:b[0])||"launch",h=p.useMemo(()=>[{name:"words",type:"string[]",default:"['LAUNCH READY', 'SYNC ONLINE', 'SIGNAL LIVE']",description:"Phrases to cycle through like a departure board."},{name:"text",type:"string",default:"undefined",description:"Optional single phrase. When set, it takes precedence over words."},{name:"flipDuration",type:"number",default:"0.12",description:"Duration, in seconds, of each individual flap turn."},{name:"stagger",type:"number",default:"0.06",description:"Delay, in seconds, between each tile starting its cascade."},{name:"cycleDelay",type:"number",default:"2400",description:"Hold time, in milliseconds, before cycling to the next phrase."},{name:"charset",type:'"alpha" | "alphanumeric" | "numeric" | string',default:'"alphanumeric"',description:"Characters used for the intermediate flip glyphs."},{name:"flipsPerChar",type:"number",default:"8",description:"Number of intermediate glyphs each changed tile flips through."},{name:"tileColor",type:"string",default:'"#111827"',description:"Base color of each mechanical tile."},{name:"textColor",type:"string",default:'"#f8fafc"',description:"Color of the glyphs on every flap."},{name:"tileRadius",type:"number | string",default:"8",description:"Corner radius for each tile."},{name:"gap",type:"number | string",default:"6",description:"Space between adjacent flap tiles."},{name:"fontSize",type:"number | string",default:"52",description:"Tile type size. Numeric values are treated as pixels."},{name:"loop",type:"boolean",default:"true",description:"Whether the phrase array repeats after the final phrase."},{name:"padTo",type:"number",default:"12",description:"Fixed tile count used to keep the board width stable."},{name:"className",type:"string",default:'""',description:"Optional class name for custom styling."},{name:"style",type:"CSSProperties",default:"{}",description:"Optional inline styles for the root element."}],[]);return e.jsx(me,{props:n,defaultProps:ne,resetProps:l,hasChanges:i,children:e.jsxs(xe,{children:[e.jsxs(ge,{children:[e.jsx(de,{className:"demo-container",minH:360,p:{base:6,md:12},overflow:"hidden",position:"relative",display:"flex",alignItems:"center",justifyContent:"center",children:e.jsx(Me,{words:d,flipDuration:y,stagger:T,cycleDelay:P,charset
1565:w,flipsPerChar:A,tileColor:M,textColor:v,tileRadius:R,gap:S,fontSize:E,loop:D,padTo:k})}),e.jsxs(ye,{children:[e.jsx(J,{title:"Phrase Set",options:ze,value:L,onChange:t=>{a("words",te[t])}}),e.jsx(J,{title:"Charset",options:Ie,value:w,onChange:t=>a("charset",t)}),e.jsx(Z,{title:"Tile Color",color:M,onChange:t=>a("tileColor",t)}),e.jsx(Z,{title:"Text Color",color:v,onChange:t=>a("textColor",t)}),e.jsx(g,{title:"Flip Duration",min:.06,max:.24,step:.01,value:y,valueUnit:"s",onChange:t=>a("flipDuration",t)}),e.jsx(g,{title:"Stagger",min:0,max:.14,step:.01,value:T,valueUnit:"s",onChange:t=>a("stagger",t)}),e.jsx(g,{title:"Cycle Delay",min:900,max:5e3,step:100,value:P,valueUnit:"ms",onChange:t=>a("cycleDelay",t)}),e.jsx(g,{title:"Flips",min:1,max:14,step:1,value:A,onChange:t=>a("flipsPerChar",t)}),e.jsx(g,{title:"Pad To",min:8,max:18,step:1,value:k,onChange:t=>a("padTo",t)}),e.jsx(g,{title:"Font Size",min:32,max:76,step:1,value:E,valueUnit:"px",onChange:t=>a("fontSize",t)}),e.jsx(g,{title:"Tile Radius",min:0,max:18,step:1,value:R,valueUnit:"px",onChange:t=>a("tileRadius",t)}),e.jsx(g,{title:"Gap",min:2,max:14,step:1,value:S,valueUnit:"px",onChange:t=>a("gap",t)}),e.jsx(Te,{title:"Loop",isChecked:D,onChange:t=>a("loop",t)})]}),e.jsx(be,{data:h}),e.jsx(Re,{dependencyList:[]})]}),e.jsx(Ce,{children:e.jsx(_e,{codeObject:Fe,componentName:"SplitFlapText"})})]})})};export{nt as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.