// shared.jsx — small reusable primitives for CAJE Collective const { useEffect, useRef, useState, useMemo, useCallback } = React; /* ----------------- Responsive hook ----------------- */ // Returns true when viewport width is at/below the breakpoint (default 768 = mobile). function useIsMobile(bp = 768) { const [m, setM] = useState(() => typeof window !== 'undefined' && window.innerWidth <= bp); useEffect(() => { const on = () => setM(window.innerWidth <= bp); on(); window.addEventListener('resize', on); return () => window.removeEventListener('resize', on); }, [bp]); return m; } /* ----------------- Reveal on scroll ----------------- */ function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' }); el.querySelectorAll('.reveal').forEach(n => io.observe(n)); return () => io.disconnect(); }, []); return ref; } /* ----------------- CAJE Wordmark ----------------- */ function Wordmark({ size = 'md', inverse = false, mono = false }) { const sizes = { sm: { caje: 16, sub: 7.5, gap: 6, letter: 0.42 }, md: { caje: 22, sub: 8.5, gap: 8, letter: 0.5 }, lg: { caje: 42, sub: 11, gap: 14, letter: 0.6 }, xl: { caje: 80, sub: 14, gap: 22, letter: 0.7 }, }; const s = sizes[size]; const color = inverse ? '#F5F0E6' : '#111111'; const sub = inverse ? 'rgba(245,240,230,0.65)' : 'rgba(17,17,17,0.55)'; const ruleColor = mono ? color : '#B89A79'; return (
C A J E
Collective
); } /* ----------------- Monogram (circular C) ----------------- */ function Monogram({ size = 54, inverse = false }) { const fg = inverse ? '#F5F0E6' : '#111111'; const bronze = '#B89A79'; return ( C ); } /* ----------------- CTA button ----------------- */ function CTA({ children, onClick, dark = false, small = false, href }) { const style = { padding: small ? '14px 24px' : '18px 32px', fontSize: small ? 10 : 11, background: dark ? '#111' : 'transparent', color: dark ? '#F5F0E6' : '#111', borderColor: dark ? '#111' : '#B89A79', }; const content = ( <> {children} ); if (href) { return {content}; } return ; } /* ----------------- Eyebrow (numbered section header) ----------------- */ function Eyebrow({ num, label, align = 'left' }) { return (
{num && ( {num} )} {label}
); } /* ----------------- Section headline (serif italic) ----------------- */ function Headline({ children, size = 'lg', as = 'h2', style = {} }) { const sizes = { sm: 38, md: 56, lg: 76, xl: 104, xxl: 140, }; const Tag = as; return ( {children} ); } /* ----------------- Image with Ken Burns ----------------- */ function KBImage({ src, alt, placeholder, ratio = '4/5', onClick, overlay = 0 }) { return (
{src ? ( {alt} ) : (
{placeholder || alt || 'image placeholder'}
)} {overlay > 0 && (
)}
); } /* ----------------- Bronze cursor glow ----------------- */ function CursorGlow({ enabled = true }) { const ref = useRef(null); useEffect(() => { if (!enabled) return; const el = ref.current; let raf; let tx = -200, ty = -200, x = -200, y = -200; const onMove = (e) => { tx = e.clientX; ty = e.clientY; if (el) el.classList.add('on'); }; const onLeave = () => { if (el) el.classList.remove('on'); }; const tick = () => { x += (tx - x) * 0.18; y += (ty - y) * 0.18; if (el) el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseleave', onLeave); raf = requestAnimationFrame(tick); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseleave', onLeave); cancelAnimationFrame(raf); }; }, [enabled]); if (!enabled) return null; return
; } /* ----------------- Lightbox ----------------- */ function Lightbox({ images, index, onClose, onNav }) { useEffect(() => { if (index == null) return; const onKey = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowRight') onNav(1); if (e.key === 'ArrowLeft') onNav(-1); }; window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [index, onClose, onNav]); if (index == null) return null; const img = images[index]; const isMobile = useIsMobile(); return (
{String(index + 1).padStart(2, '0')} / {String(images.length).padStart(2, '0')}
{img.caption}
{img.src ? ( {img.caption} ) : (
{img.caption}
)}
); } function navBtn(side, isMobile) { return { position: 'absolute', [side]: isMobile ? '50%' : 30, bottom: isMobile ? 20 : 'auto', top: isMobile ? 'auto' : '50%', transform: isMobile ? `translateX(${side === 'left' ? '-70px' : '10px'})` : 'translateY(-50%)', width: 54, height: 54, borderRadius: '50%', background: isMobile ? 'rgba(17,17,17,0.6)' : 'transparent', border: '1px solid rgba(184,154,121,0.5)', color: 'rgba(245,240,230,0.85)', fontSize: 28, fontFamily: 'EB Garamond, serif', cursor: 'pointer', transition: 'all 0.4s', }; } /* ----------------- Marquee line ----------------- */ function TickerStrip({ items }) { return (
{[...items, ...items].map((t, i) => ( {t} ))}
); } Object.assign(window, { useReveal, useIsMobile, Wordmark, Monogram, CTA, Eyebrow, Headline, KBImage, CursorGlow, Lightbox, TickerStrip, });