// Shared utilities and primitives
const { useState, useEffect, useRef, useCallback } = React;
// Reveal-on-scroll hook with fallbacks for environments where
// IntersectionObserver doesn't fire reliably (iframes, etc.)
const useReveal = () => {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
let revealed = false;
const reveal = () => {
if (revealed) return;
revealed = true;
el.classList.add("is-in");
};
// Immediate check — if element is already in viewport on mount, reveal now.
const checkVisible = () => {
const r = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
if (r.top < vh - 40 && r.bottom > 0) reveal();
};
checkVisible();
// IntersectionObserver for off-screen elements as the user scrolls.
let obs;
if (typeof IntersectionObserver !== "undefined") {
obs = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
reveal();
obs.unobserve(e.target);
}
});
}, { threshold: 0.05, rootMargin: "0px 0px -40px 0px" });
obs.observe(el);
}
// Scroll fallback in case IO doesn't fire
const onScroll = () => { checkVisible(); };
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
if (obs) obs.disconnect();
window.removeEventListener("scroll", onScroll);
};
}, []);
return ref;
};
// Reveal wrapper component
const Reveal = ({ children, delay = 0, as: Tag = "div", style, ...rest }) => {
const ref = useReveal();
return (
{children}
);
};
// Demo button — opens the modal
const DemoButton = ({ children, variant = "primary", size = "md", style, onClick }) => {
const handleClick = (e) => {
// ripple
const btn = e.currentTarget;
const rect = btn.getBoundingClientRect();
const r = document.createElement("span");
r.className = "ripple";
const size = Math.max(rect.width, rect.height);
r.style.width = r.style.height = size + "px";
r.style.left = (e.clientX - rect.left - size / 2) + "px";
r.style.top = (e.clientY - rect.top - size / 2) + "px";
btn.appendChild(r);
setTimeout(() => r.remove(), 600);
if (onClick) onClick(e);
else window.dispatchEvent(new CustomEvent("open-demo"));
};
const padBySize = size === "lg" ? "18px 30px" : size === "sm" ? "10px 18px" : "14px 22px";
const fsBySize = size === "lg" ? 17 : size === "sm" ? 14 : 16;
return (
);
};
// Section eyebrow
const Eyebrow = ({ color = "var(--neon-purple-2)", children, style }) => (
{children}
);
// Inline icon — minimal stroked SVG to match Lucide-ish look
const Icon = ({ name, size = 24, color = "currentColor", strokeWidth = 1.75 }) => {
const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" };
const paths = {
"phone-call": <>>,
"shield": ,
"lock": <>>,
"message": ,
"plug": <>>,
"settings": <>>,
"rocket": <>>,
"check": ,
"plus": <>>,
"arrow-right": <>>,
"menu": <>>,
"x": <>>,
"sparkles": <>>,
"trending-up": <>>,
"facebook": ,
"linkedin": <>>,
"youtube": <>>,
"whatsapp": <>>,
"zap": ,
};
return ;
};
// Wordmark
const Logo = ({ height = 32 }) => (
eVocal.ro
);
// HoverBorderGradient button — Aceternity-port, no framer-motion
// Usage: or
function HoverBorderBtn({ children, href, onClick, pill, className = "", btnClassName = "", btnStyle = {}, style = {} }) {
const inner = (
);
return (
);
}
// PointerHighlight — Aceternity-port, no framer-motion.
// Uses SVG stroke-dashoffset for border draw + CSS transitions for pointer movement.
const { useRef: useRefPH, useState: useStatePH, useEffect: useEffectPH } = React;
function PointerHighlight({ children, color = "rgba(87,255,135,0.85)", pointerColor = "#c5ff5e", pad = 5 }) {
const ref = useRefPH(null);
const [dims, setDims] = useStatePH({ w: 0, h: 0 });
const [visible, setVisiblePH] = useStatePH(false);
useEffectPH(() => {
const measure = () => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
setDims({ w: r.width, h: r.height });
};
measure();
const ro = new ResizeObserver(measure);
if (ref.current) ro.observe(ref.current);
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setVisiblePH(true); io.disconnect(); }
}, { threshold: 0.4 });
if (ref.current) io.observe(ref.current);
return () => { ro.disconnect(); io.disconnect(); };
}, []);
const bw = dims.w + pad * 2;
const bh = dims.h + pad * 2;
const perimeter = 2 * (bw + bh);
return (
{children}
{dims.w > 0 && (
<>
{/* Drawing border */}
{/* Pointer cursor */}
>
)}
);
}
Object.assign(window, { useReveal, Reveal, DemoButton, Eyebrow, Icon, Logo, HoverBorderBtn, PointerHighlight });