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.