/* Shared hooks, icons, and small components */ const { useState, useEffect, useRef, useCallback } = React; /* ---- Hook: reveal on scroll (scroll-math based, robust) ---- */ function useReveal(options) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const trigger = (options && options.trigger) || 0.9; // reveal when top crosses 90% of viewport let raf = 0; let done = false; const check = () => { raf = 0; if (done) return; const r = el.getBoundingClientRect(); if (r.top < window.innerHeight * trigger && r.bottom > 0) { done = true; setShown(true); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); } }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(check); }; check(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); return [ref, shown]; } /* ---- Reveal wrapper ---- */ function Reveal({ children, delay = 0, as = "div", className = "", style = {}, ...rest }) { const [ref, shown] = useReveal(); const Tag = as; return ( {children} ); } /* ---- Hook: animated counter when in view ---- */ function useCountUp(target, duration = 1600) { const ref = useRef(null); const [val, setVal] = useState(0); const started = useRef(false); useEffect(() => { const el = ref.current; if (!el) return; let raf = 0; const begin = () => { const start = performance.now(); const tick = (now) => { const p = Math.min((now - start) / duration, 1); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(eased * target)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }; const check = () => { raf = 0; if (started.current) return; const r = el.getBoundingClientRect(); if (r.top < window.innerHeight * 0.85 && r.bottom > 0) { started.current = true; begin(); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); } }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(check); }; check(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); if (raf) cancelAnimationFrame(raf); }; }, [target, duration]); return [ref, val]; } /* ---- Hook: rotating role text with typing ---- */ function useTypewriter(words, { typeSpeed = 80, deleteSpeed = 40, hold = 1600 } = {}) { const [text, setText] = useState(""); const [idx, setIdx] = useState(0); const [deleting, setDeleting] = useState(false); useEffect(() => { const full = words[idx % words.length]; let timeout; if (!deleting && text === full) { timeout = setTimeout(() => setDeleting(true), hold); } else if (deleting && text === "") { setDeleting(false); setIdx((i) => (i + 1) % words.length); } else { timeout = setTimeout(() => { setText((t) => deleting ? full.slice(0, t.length - 1) : full.slice(0, t.length + 1) ); }, deleting ? deleteSpeed : typeSpeed); } return () => clearTimeout(timeout); }, [text, deleting, idx, words]); return text; } /* ---- Section heading ---- */ function SectionHeading({ eyebrow, title, lead, align = "left" }) { return ( {eyebrow}

{lead &&

{lead}

} ); } /* ---- Icons ---- */ const Icon = { arrow: (p) => (), download: (p) => (), mail: (p) => (), github: (p) => (), linkedin: (p) => (), map: (p) => (), bolt: (p) => (), layers: (p) => (), server: (p) => (), database: (p) => (), cloud: (p) => (), cap: (p) => (), award: (p) => (), }; const skillIconFor = (group) => ({ "Frontend": Icon.layers, "Backend & APIs": Icon.server, "Data": Icon.database, "Cloud & DevOps": Icon.cloud, }[group] || Icon.bolt); Object.assign(window, { useReveal, Reveal, useCountUp, useTypewriter, SectionHeading, Icon, skillIconFor });