/* Shared helpers — НефтьОргХим+ site. */ const { useState, useEffect, useRef } = React; const REDUCED = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches; function useReveal(delay = 0, y = 24) { const ref = useRef(null); const [on, setOn] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; if (REDUCED()) return setOn(true); const io = new IntersectionObserver(([e]) => setOn(e.isIntersecting), { threshold: 0.1, rootMargin: "0px 0px -60px 0px" }); io.observe(el); return () => io.disconnect(); }, []); return [ref, { opacity: on ? 1 : 0, transform: on ? "none" : `translateY(${y}px)`, filter: on ? "none" : "blur(10px)", transition: `opacity 900ms var(--ease-out) ${delay}ms, transform 900ms var(--ease-out) ${delay}ms, filter 900ms var(--ease-out) ${delay}ms` }]; } function Reveal({ delay = 0, y, as: Tag = "div", style, children, ...rest }) { const [ref, rs] = useReveal(delay, y); return {children}; } /** Word-by-word blur reveal for display headings. */ function RevealText({ text, delay = 0, step = 70, style, as: Tag = "span" }) { const [ref, rs] = useReveal(delay, 0); const on = rs.opacity === 1; return ( {text.split(" ").map((w, i) => ( {w + " "} ))} ); } function useScrollY() { const [y, setY] = useState(0); useEffect(() => { let t = false; const on = () => { if (t) return; t = true; requestAnimationFrame(() => { setY(window.scrollY); t = false; }); }; window.addEventListener("scroll", on, { passive: true }); on(); return () => window.removeEventListener("scroll", on); }, []); return y; } /** Progress 0→1 of an element travelling through the viewport (for gentle parallax). */ function useSectionProgress(ref) { const y = useScrollY(); const [p, setP] = useState(0.5); useEffect(() => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); setP(Math.max(0, Math.min(1, (window.innerHeight - r.top) / (window.innerHeight + r.height)))); }, [y]); return p; } /** Very small pointer parallax, in px. */ function usePointer(strength = 12) { const [p, setP] = useState({ x: 0, y: 0 }); useEffect(() => { if (REDUCED() || window.matchMedia("(pointer: coarse)").matches) return; const on = (e) => setP({ x: (e.clientX / window.innerWidth - 0.5) * strength, y: (e.clientY / window.innerHeight - 0.5) * strength }); window.addEventListener("pointermove", on, { passive: true }); return () => window.removeEventListener("pointermove", on); }, []); return p; } function useActiveSection(ids) { const [active, setActive] = useState(ids[0]); useEffect(() => { const io = new IntersectionObserver((entries) => { const vis = entries.filter((e) => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; if (vis) setActive(vis.target.id); }, { rootMargin: "-45% 0px -45% 0px", threshold: [0.01, 0.3] }); ids.forEach((id) => { const el = document.getElementById(id); if (el) io.observe(el); }); return () => io.disconnect(); }, []); return active; } function Counter({ to, suffix = "", duration = 1600 }) { const ref = useRef(null); const [v, setV] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (!e.isIntersecting) return; io.disconnect(); const t0 = performance.now(); const step = (now) => { const p = Math.min((now - t0) / duration, 1); setV(p >= 1 ? to : Math.round(to * (1 - Math.pow(1 - p, 3)))); if (p < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); }, { threshold: 0.6 }); io.observe(el); return () => io.disconnect(); }, [to]); return {v}{suffix}; } function Section({ id, children, style, inner, ...rest }) { return (
{children}
); } const Icon = ({ name, size = 40, color = "currentColor", style }) => (