/* QUITOS — Primitives & layout components */
const { useState, useEffect, useRef } = React;
/* Lucide icon helper */
function Icon({ name, size = 20, color, strokeWidth = 1.5, style }) {
const ref = useRef(null);
useEffect(() => {
if (window.lucide && ref.current) window.lucide.createIcons({ nameAttr: 'data-lucide' });
}, [name]);
return (
);
}
/* Scroll-reveal wrapper — GSAP ScrollTrigger */
function Reveal({ children, delay = 0, as: As = 'div', className = '', ...rest }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el || !window.gsap) return;
gsap.set(el, { opacity: 0, y: 28 });
const trigger = ScrollTrigger.create({
trigger: el,
start: 'top 88%',
onEnter: () => {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.65,
delay: delay / 1000,
ease: 'power3.out',
clearProps: 'opacity,transform',
});
},
once: true,
});
return () => trigger.kill();
}, [delay]);
return {children};
}
/* Stagger — reveals direct children one by one */
function Stagger({ children, staggerDelay = 0.1, as: As = 'div', className = '', ...rest }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el || !window.gsap) return;
const items = Array.from(el.children);
gsap.set(items, { opacity: 0, y: 32 });
const trigger = ScrollTrigger.create({
trigger: el,
start: 'top 85%',
onEnter: () => {
gsap.to(items, {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power3.out',
stagger: staggerDelay,
clearProps: 'opacity,transform',
});
},
once: true,
});
return () => trigger.kill();
}, [staggerDelay]);
return {children};
}
/* Counter-up metric — GSAP tween on scroll */
function Counter({ value, suffix = '', prefix = '', duration = 1.4, formatter }) {
const ref = useRef(null);
const objRef = useRef({ n: 0 });
const [display, setDisplay] = useState(prefix + "0" + suffix);
useEffect(() => {
const el = ref.current;
if (!el || !window.gsap) return;
const trigger = ScrollTrigger.create({
trigger: el,
start: 'top 85%',
onEnter: () => {
gsap.to(objRef.current, {
n: value,
duration,
ease: 'power2.out',
onUpdate: () => {
const n = Math.round(objRef.current.n);
const formatted = formatter ? formatter(n) : n.toLocaleString('fr-FR');
setDisplay(prefix + formatted + suffix);
},
});
},
once: true,
});
return () => trigger.kill();
}, [value, duration, prefix, suffix, formatter]);
return {display};
}
/* Top nav */
function Nav({ dark = true }) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
const bg = dark
? (scrolled ? 'rgba(10,10,10,0.88)' : 'transparent')
: (scrolled ? 'rgba(248,248,246,0.92)' : 'transparent');
const textColor = dark ? '#fff' : '#0A0A0A';
return (
);
}
/* Animated brand logo — scroll-trigger appear + hover glow */
function AnimatedBrandLogo({ name, color }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el || !window.gsap) return;
gsap.set(el, { opacity: 0, y: 14 });
const trigger = ScrollTrigger.create({
trigger: el, start: 'top 92%',
onEnter: () => gsap.to(el, { opacity: 0.45, y: 0, duration: 0.7, ease: 'power3.out' }),
once: true,
});
return () => trigger.kill();
}, []);
return (
{
e.currentTarget.style.opacity = '1';
e.currentTarget.style.filter = `drop-shadow(0 0 10px ${color}88)`;
}}
onMouseLeave={e => {
e.currentTarget.style.opacity = '0.45';
e.currentTarget.style.filter = 'none';
}}
>
{name}
);
}
Object.assign(window, { Icon, Reveal, Stagger, Counter, Nav, FeatureIcon, MagneticBtn, AnimatedBrandLogo });