/* global React */ const { useState, useEffect, useRef } = React; // Renders with WebP source + original as fallback for local images. // External URLs (http/https) and unknown extensions pass through as plain . function Img({ src, ...rest }) { if (!src || /^(https?:)?\/\//.test(src) || !/\.(jpe?g|png)$/i.test(src)) { return ; } const webp = src.replace(/\.(jpe?g|png)$/i, '.webp'); return ( ); } // "+" cross icon for pill button const XIco = () => ( ); function useMagnetic(strength = 0.32, enabled = true, radius = 140) { const ref = useRef(null); useEffect(() => { if (!enabled) return; if (window.matchMedia('(pointer: coarse)').matches) return; const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; const dist = Math.hypot(e.clientX - cx, e.clientY - cy); if (dist > radius) { el.style.transform = ''; return; } const falloff = 1 - dist / radius; const dx = (e.clientX - cx) * strength * falloff; const dy = (e.clientY - cy) * strength * falloff; el.style.transform = `translate(${dx}px, ${dy}px)`; }; const onLeave = () => { el.style.transform = ''; }; document.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { document.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength, enabled, radius]); return ref; } function Btn({ children, onClick, outline, magnet = true, lg = false, className = '' }) { const ref = useMagnetic(0.32, magnet); return ( ); } function Tag({ children }) { return {children}; } function MagneticNavLink({ children, active, onClick }) { const ref = useMagnetic(0.24, true, 75); return {children}; } function Magnetic({ children, as: T = 'a', className = '', strength = 0.22, radius = 65, onClick, href, ...props }) { const ref = useMagnetic(strength, true, radius); const isClickable = !!onClick && !href; const handleKey = isClickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); } } : undefined; return ( {children} ); } function StudioStatus() { const [now, setNow] = useState(() => new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 60000); return () => clearInterval(id); }, []); const time = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' }).format(now); const hour = parseInt(new Intl.DateTimeFormat('en-GB', { hour: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(now), 10); const weekday = new Intl.DateTimeFormat('en-GB', { weekday: 'short', timeZone: 'Europe/Berlin' }).format(now); const isWeekday = !['Sat', 'Sun'].includes(weekday); const isOpen = isWeekday && hour >= 9 && hour < 19; return (
{isOpen ? 'Studio aktiv' : 'Off-Hours'} · Köln {time}
); } function Header({ active, onNav }) { const items = [ { id: 'home', label: 'home', href: '/' }, { id: 'creative', label: 'about', href: '/about/' }, { id: 'projects', label: 'cases', href: '/cases/' }, { id: 'kontakt', label: 'kontakt', href: '/kontakt/' }, ]; const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { document.body.classList.toggle('nav-open', menuOpen); return () => document.body.classList.remove('nav-open'); }, [menuOpen]); const goAndClose = (id) => { setMenuOpen(false); onNav(id); }; return ( <>
{ setMenuOpen(false); onNav('home'); }} strength={0.3} radius={90}>Cuvir®
onNav('kontakt')}>Termin buchen
{ e.preventDefault(); goAndClose('kontakt'); }} className="mobile-menu-cta">Termin buchen →
); } function Marquee({ items, paused, outline }) { const list = items.concat(items); return (
{list.map((it, i) => ( {it} ))}
); } function ScrollCounter({ to, label, num }) { const [val, setVal] = useState(0); const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const target = parseFloat(String(to).replace(/[^\d.]/g, '')) || 0; const obs = new IntersectionObserver(([e]) => { if (!e.isIntersecting) return; const start = performance.now(); const dur = 1300; const tick = (t) => { const p = Math.min(1, (t - start) / dur); const eased = 1 - Math.pow(1 - p, 3); setVal(target * eased); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); obs.disconnect(); }, { threshold: 0.4 }); obs.observe(el); return () => obs.disconnect(); }, [to]); const display = (() => { const target = parseFloat(String(to).replace(/[^\d.]/g, '')) || 0; const isInt = Number.isInteger(target); const n = isInt ? Math.round(val) : val.toFixed(1); return String(to).replace(/[\d.]+/, n); })(); return (
{num}
{display}
{label}
); } const SCRAMBLE_POOL = '/\\<>{}[]+=#$%&*?ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; function setupScramble(root) { const chars = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; let n; while ((n = walker.nextNode())) if (n.nodeValue && n.nodeValue.trim()) nodes.push(n); nodes.forEach(node => { const frag = document.createDocumentFragment(); [...node.nodeValue].forEach(c => { if (c === ' ') { frag.appendChild(document.createTextNode(' ')); return; } const span = document.createElement('span'); span.className = 'scr-c'; span.dataset.final = c; span.textContent = c; chars.push(span); frag.appendChild(span); }); node.parentNode.replaceChild(frag, node); }); chars.forEach(c => { c.textContent = SCRAMBLE_POOL[Math.floor(Math.random() * SCRAMBLE_POOL.length)]; }); return chars; } function runScramble(chars) { chars.forEach((span, i) => { const final = span.dataset.final; if (/[.,!?·\-:;—]/.test(final)) { span.textContent = final; return; } const start = i * 28 + 50; const ticks = 6 + Math.floor(Math.random() * 5); let n = 0; setTimeout(() => { const id = setInterval(() => { if (n >= ticks) { clearInterval(id); span.textContent = final; } else { span.textContent = SCRAMBLE_POOL[Math.floor(Math.random() * SCRAMBLE_POOL.length)]; n++; } }, 40); }, start); }); } function setupWordReveal(root) { const words = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; let n; while ((n = walker.nextNode())) if (n.nodeValue && n.nodeValue.trim()) nodes.push(n); nodes.forEach(node => { const frag = document.createDocumentFragment(); const parts = node.nodeValue.split(/(\s+)/); parts.forEach(part => { if (!part) return; if (/^\s+$/.test(part)) { frag.appendChild(document.createTextNode(part)); } else { const outer = document.createElement('span'); outer.className = 'word-reveal'; const inner = document.createElement('span'); inner.className = 'word-reveal-inner'; inner.textContent = part; outer.appendChild(inner); words.push(inner); frag.appendChild(outer); } }); node.parentNode.replaceChild(frag, node); }); words.forEach((w, i) => { w.style.animationDelay = `${i * 70}ms`; }); } function Reveal({ children, delay = 0, scramble = false, wordReveal = false, as: Tg = 'div', className = '', ...rest }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const motionOff = document.body.classList.contains('no-motion'); const chars = (scramble && !motionOff) ? setupScramble(el) : null; if (wordReveal && !motionOff && !scramble) setupWordReveal(el); const obs = new IntersectionObserver(([e]) => { if (!e.isIntersecting) return; setTimeout(() => { el.classList.add('in'); if (chars) runScramble(chars); }, delay); obs.disconnect(); }, { threshold: 0.15 }); obs.observe(el); return () => obs.disconnect(); }, [delay, scramble, wordReveal]); return {children}; } function Footer({ onNav }) { const bigRef = useRef(null); useEffect(() => { const el = bigRef.current; if (!el) return; const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { el.classList.add('in'); obs.disconnect(); } }, { threshold: 0.25 }); obs.observe(el); return () => obs.disconnect(); }, []); return ( ); } function CustomCursor() { const dotRef = useRef(null); const ringRef = useRef(null); useEffect(() => { if (window.matchMedia('(pointer: coarse)').matches) return; const dot = dotRef.current, ring = ringRef.current; if (!dot || !ring) return; let tx = -100, ty = -100, dx = -100, dy = -100, rx = -100, ry = -100, raf = 0, hoverOn = false, frame = 0; const interactiveSel = 'a, button, [role="button"], input, textarea, select, .case, .svc-card, .price, .stream-cell, .footer ul li, summary'; const onMove = (e) => { tx = e.clientX; ty = e.clientY; }; let hidden = false; const updateHover = () => { const el = document.elementFromPoint(tx, ty); const overIframe = !!(el && (el.tagName === 'IFRAME' || (el.closest && el.closest('.terminal-window')))); if (overIframe !== hidden) { hidden = overIframe; ring.classList.toggle('fade', overIframe); dot.classList.toggle('fade', overIframe); } if (overIframe) return; const interactive = !!(el && el.closest && el.closest(interactiveSel)); if (interactive !== hoverOn) { hoverOn = interactive; ring.classList.toggle('on', interactive); dot.classList.toggle('on', interactive); } }; const tick = () => { dx += (tx - dx) * 0.45; dy += (ty - dy) * 0.45; rx += (tx - rx) * 0.18; ry += (ty - ry) * 0.18; dot.style.transform = `translate(${dx}px, ${dy}px)`; ring.style.transform = `translate(${rx}px, ${ry}px)`; if ((frame++ & 1) === 0) updateHover(); raf = requestAnimationFrame(tick); }; document.addEventListener('mousemove', onMove); raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); document.removeEventListener('mousemove', onMove); }; }, []); return ( <>
); } function FloatingCTA({ onNav, route }) { const [show, setShow] = useState(false); useEffect(() => { const isKontakt = route === 'kontakt'; const isLegal = route.startsWith('legal:') || ['impressum','agb','datenschutz'].includes(route); if (isKontakt || isLegal) { setShow(false); return; } let footerInView = false; const update = () => { setShow(window.scrollY > 500 && !footerInView); }; let obs = null; const setupObserver = () => { const footer = document.querySelector('.footer'); if (!footer) { setTimeout(setupObserver, 200); return; } obs = new IntersectionObserver(([e]) => { footerInView = e.isIntersecting; update(); }, { threshold: 0, rootMargin: '0px 0px -40px 0px' }); obs.observe(footer); }; setupObserver(); window.addEventListener('scroll', update, { passive: true }); update(); return () => { window.removeEventListener('scroll', update); if (obs) obs.disconnect(); }; }, [route]); return ( ); } function CookieBanner({ onNav }) { const [show, setShow] = useState(false); useEffect(() => { try { // Only 'accepted' is persistent. Rejection is session-only — banner reappears next visit. const accepted = localStorage.getItem('cuvir-cookie-consent') === 'accepted'; const sessionDismissed = sessionStorage.getItem('cuvir-cookie-session-dismissed') === '1'; if (!accepted && !sessionDismissed) { const t = setTimeout(() => setShow(true), 800); return () => clearTimeout(t); } } catch (e) {} }, []); const accept = () => { try { localStorage.setItem('cuvir-cookie-consent', 'accepted'); sessionStorage.removeItem('cuvir-cookie-session-dismissed'); if (window.gtag) window.gtag('consent', 'update', { analytics_storage: 'granted' }); } catch (e) {} setShow(false); }; const reject = () => { try { // Don't persist rejection — banner re-appears on next visit localStorage.removeItem('cuvir-cookie-consent'); sessionStorage.setItem('cuvir-cookie-session-dismissed', '1'); } catch (e) {} setShow(false); }; if (!show) return null; return (
/ Cookie-Einstellungen

Wir nutzen Cookies für anonyme Analyse (Google Analytics) und um die Nutzung der Seite zu verbessern. Details in unserer{' '} { e.preventDefault(); onNav && onNav('legal:datenschutz'); }} className="cookie-banner-link" >Datenschutzerklärung.

); } function OutlineMarquee({ text, paused = false, speed = 40 }) { const items = Array(6).fill(text); return (
{[...items, ...items].map((t, i) => ( {t} ))}
); } Object.assign(window, { Btn, Tag, Header, Footer, Marquee, ScrollCounter, Reveal, useMagnetic, CustomCursor, CookieBanner, OutlineMarquee, FloatingCTA });