/* 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 (
);
}
const Icon = ({ name, size = 40, color = "currentColor", style }) => (
);
/** Slow steel ticker — used once, under the hero. */
function Ticker({ items, duration = 64 }) {
const row = items.concat(items);
return (
{row.map((t, i) => (
{t}
))}
);
}
Object.assign(window, { useReveal, Reveal, RevealText, useScrollY, useSectionProgress, usePointer, useActiveSection, Counter, Section, Icon, Ticker });