/* global React */
const { useState, useEffect, useRef } = React;
/* ======================================================
Cursor — small, mix-blend
====================================================== */
function Cursor() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
let x = window.innerWidth / 2, y = window.innerHeight / 2;
let tx = x, ty = y;
const onMove = (e) => { tx = e.clientX; ty = e.clientY; };
const onOver = (e) => {
const t = e.target;
if (t.closest && t.closest('a, button, .svc, .case, .contact-email')) el.classList.add('hover');
else el.classList.remove('hover');
};
const tick = () => {
x += (tx - x) * 0.2;
y += (ty - y) * 0.2;
el.style.transform = `translate(${x}px, ${y}px) translate(-50%,-50%)`;
requestAnimationFrame(tick);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseover', onOver);
tick();
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseover', onOver);
};
}, []);
return
;
}
/* ======================================================
Live clock — São Paulo
====================================================== */
function Clock() {
const [t, setT] = useState('');
useEffect(() => {
const fmt = () => {
const d = new Date();
const opts = { hour: '2-digit', minute: '2-digit', timeZone: 'America/Sao_Paulo', hour12: false };
setT(new Intl.DateTimeFormat('pt-BR', opts).format(d));
};
fmt();
const id = setInterval(fmt, 1000 * 15);
return () => clearInterval(id);
}, []);
return RS / {t};
}
/* ======================================================
LANGUAGE SWITCHER — BR · EN · ES (live site translation)
====================================================== */
function LangSwitcher() {
const [state, setState] = React.useState({ lang: 'pt', busy: false });
React.useEffect(() => {
if (!window.__i18n) return;
return window.__i18n.subscribe(setState);
}, []);
const LANGS = [['pt', 'BR'], ['en', 'EN'], ['es', 'ES']];
return (
{LANGS.map(([code, label]) => (
))}
{state.busy && }
);
}
/* ======================================================
Reveal on scroll
====================================================== */
function useReveal() {
useEffect(() => {
const els = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
}, { threshold: 0.01, rootMargin: '0px 0px 0px 0px' });
els.forEach(el => io.observe(el));
// Safety net: force-reveal anything still hidden after 2.5s
const t = setTimeout(() => {
document.querySelectorAll('.reveal:not(.in)').forEach(el => el.classList.add('in'));
}, 2500);
return () => { io.disconnect(); clearTimeout(t); };
}, []);
}
/* ======================================================
NAV
====================================================== */
function Nav() {
return (
);
}
/* ======================================================
HERO
====================================================== */
function Hero() {
return (
);
}
/* ======================================================
MORSE SIGNAL — live transmitter spelling "MORSE"
====================================================== */
function MorseSignal() {
// M O R S E → letters as dot/dash sequences
const WORD = [
['dash', 'dash'], // M
['dash', 'dash', 'dash'], // O
['dot', 'dash', 'dot'], // R
['dot', 'dot', 'dot'], // S
['dot'], // E
];
// flatten with letter index for gap logic
const symbols = [];
WORD.forEach((letter, li) => letter.forEach((t) => symbols.push({ type: t, li })));
const [active, setActive] = React.useState(-1);
React.useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) { setActive(-1); return; }
const unit = 135;
let idx = 0;
let timer;
const run = () => {
const sym = symbols[idx];
setActive(idx);
const onDur = sym.type === 'dash' ? unit * 3 : unit;
timer = setTimeout(() => {
setActive(-1);
const next = symbols[idx + 1];
let gap = unit; // inter-symbol
if (!next) gap = unit * 7; // word reset
else if (next.li !== sym.li) gap = unit * 3; // inter-letter
timer = setTimeout(() => {
idx = (idx + 1) % symbols.length;
run();
}, gap);
}, onDur);
};
run();
return () => clearTimeout(timer);
}, []);
let gi = -1;
return (
Transmitting
{WORD.map((letter, li) => (
{letter.map((t) => {
gi += 1;
const cur = gi;
return (
);
})}
))}
);
}
function Marquee() {
const items = [
'Strategy', 'Brand Design',
'Editorial & Information Design', 'Culture & Institutional Communication',
'Digital Experience', 'Content & Social Systems',
'AI Creative Studio',
];
const row = (
<>
{items.map((it, i) => (
✦
{it}
))}
>
);
return (
);
}
window.Cursor = Cursor;
window.Clock = Clock;
window.Nav = Nav;
window.Hero = Hero;
window.useReveal = useReveal;