// 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 {paths[name] || null}; }; // 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 (
{href ? {inner} : inner }
); } // 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 });