/* ============================================================
Shared components + scroll hooks
============================================================ */
const { useState, useEffect, useRef, useCallback } = React;
/* ---------- Gray placeholder (or real image when src is set) ---------- */
function Placeholder({ tone = "1", label, parallax = false, zoom = false, speed = 0.12, style, className = "", src }) {
const ref = useRef(null);
useEffect(() => {
if (!parallax) return;
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
let raf = null;
const inner = el.querySelector(".ph__inner");
const update = () => {
raf = null;
const amount = window.__PARALLAX_AMOUNT ?? 1;
if (!inner) return;
if (amount === 0) { inner.style.transform = ""; return; }
const r = el.getBoundingClientRect();
const vh = window.innerHeight;
const center = r.top + r.height / 2;
const prog = (center - vh / 2) / (vh / 2 + r.height / 2); // -1..1, 0 centered
const ty = -prog * speed * 100 * amount;
let tr = `translate3d(0, ${ty.toFixed(2)}px, 0)`;
if (zoom) {
const sc = 1 + Math.min(0.09, Math.abs(prog) * 0.09);
tr += ` scale(${sc.toFixed(3)})`;
}
inner.style.transform = tr;
};
const onScroll = () => { if (raf == null) raf = requestAnimationFrame(update); };
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, [parallax, zoom, speed]);
return (
{src &&

}
{label && !src ?
{label} : null}
);
}
/* ---------- Reveal-on-scroll wrapper ---------- */
function Reveal({ children, as = "div", delay = 0, className = "", style, fromRight = false }) {
const ref = useRef(null);
const [shown, setShown] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) { setShown(true); return; }
let done = false;
let raf = null;
const check = () => {
raf = null;
if (done || !ref.current) return;
const r = ref.current.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
// in view once its top crosses ~94% of viewport height
if (r.top < vh * 0.94 && r.bottom > 0) {
done = true;
setTimeout(() => setShown(true), delay);
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
}
};
const onScroll = () => { if (raf == null) raf = requestAnimationFrame(check); };
// run once synchronously (rAF is throttled in offscreen iframes)
check();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, [delay]);
const Tag = as;
return {children};
}
/* ---------- Social icons (line) ---------- */
const ICONS = {
instagram: <>>,
dribbble: <>>,
linkedin: <>>,
read: <>>,
};
function SocialIcon({ name }) {
return (
);
}
function Socials({ names = ["instagram", "dribbble", "linkedin", "read"] }) {
return (
);
}
/* ---------- Lang toggle (top-right of card) ---------- */
function LangToggle({ lang, setLang }) {
return (
);
}
/* ---------- Back button (circle arrow) ---------- */
function BackButton({ onClick, label }) {
return (
);
}
Object.assign(window, { Placeholder, Reveal, Socials, SocialIcon, LangToggle, BackButton });