// W2Go motion + skeleton + visual primitives const { useState: useStateM, useEffect: useEffectM, useRef: useRefM } = React; // ─── Skeleton loaders ────────────────────────────────────────── function Skeleton({ w = '100%', h = 16, r = 8, style = {} }) { return
; } function SkeletonCard() { return (
); } // ─── Fade/slide entrance ─────────────────────────────────────── // Stagger child elements as they enter. function Stagger({ children, delay = 60, dir = 'up', enabled = true }) { const kids = React.Children.toArray(children); return <>{kids.map((c, i) => (
{c}
))}; } // ─── Image with premium treatment + fade-in ──────────────────── // Slight vignette + warm color grade overlay, smooth fade as it loads. function LuxImage({ src, alt = '', height = 'auto', width = '100%', radius = 0, vignette = true, grade = 'warm', style = {} }) { const [loaded, setLoaded] = useStateM(false); return (
{!loaded &&
} {alt} setLoaded(true)} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: loaded ? 1 : 0, transition: 'opacity 500ms cubic-bezier(0.2,0.7,0.2,1)', filter: grade === 'warm' ? 'saturate(1.05) contrast(1.04)' : 'saturate(0.95)', }}/> {vignette &&
}
); } // ─── Marquee text reveal (for hero) ──────────────────────────── function RevealText({ text, size = 30, weight = 800, color = 'inherit', delay = 0, ls = -0.6 }) { // Split by line breaks; each line is its own clip-reveal. const lines = text.split('\n'); return (
{lines.map((l, i) => (
{l || '\u00A0'}
))}
); } // ─── Bottom sheet that animates in ───────────────────────────── function BottomSheet({ open, onClose, children, height = 'auto' }) { if (!open) return null; return (
e.stopPropagation()} style={{ width: '100%', background: 'var(--w-cream)', borderRadius: '24px 24px 0 0', padding: '14px 18px 30px', animation: 'w-sheet-up .35s cubic-bezier(0.2,0.7,0.2,1)', maxHeight: height, overflowY: 'auto', boxShadow: '0 -12px 30px rgba(0,0,0,0.18)' }}>
{children}
); } // ─── Animations CSS (injected once) ──────────────────────────── if (!document.getElementById('w-anim-css')) { const s = document.createElement('style'); s.id = 'w-anim-css'; s.textContent = ` @keyframes w-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } @keyframes w-enter-up { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } } @keyframes w-enter-down { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } } @keyframes w-enter-fade { from { opacity: 0; } to { opacity: 1; } } @keyframes w-enter-scale{ from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: none; } } @keyframes w-reveal { from { transform: translateY(100%); } to { transform: none; } } @keyframes w-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes w-sheet-up { from { transform: translateY(100%); } to { transform: none; } } @keyframes w-pulse-soft { 0%,100% { opacity: 1; } 50% { opacity: 0.55; } } @keyframes w-spin { to { transform: rotate(360deg); } } @keyframes w-bump { 0%,100% { transform: scale(1); } 30% { transform: scale(1.18); } 60% { transform: scale(0.96); } } @keyframes w-pop { 0% { transform: scale(0); opacity: 0; } 60% { transform: scale(1.12); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes w-pulse-ring { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } } @keyframes w-dot-jump { 0%, 60%, 100% { transform: translateY(0); opacity: .4; } 30% { transform: translateY(-4px); opacity: 1; } } `; document.head.appendChild(s); } // ─── Typing/loading dots ─────────────────────────────────────── function TypingDots({ color = 'currentColor' }) { return ( {[0,1,2].map(i => )} ); } Object.assign(window, { Skeleton, SkeletonCard, Stagger, LuxImage, RevealText, BottomSheet, TypingDots });