/* global React */ const { useState, useEffect, useRef, useMemo, useCallback } = React; /* ============================================================ useScrollRevealAll — adds .in class to .reveal nodes ============================================================ */ function useScrollReveal() { useEffect(() => { const io = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } } }, { threshold: 0.08, rootMargin: "0px 0px -4% 0px" } ); const observeAll = () => { document .querySelectorAll(".reveal:not(.in), .text-reveal:not(.in)") .forEach((el) => io.observe(el)); }; observeAll(); // Re-observe whenever new nodes appear (route changes) const mo = new MutationObserver(observeAll); mo.observe(document.body, { childList: true, subtree: true }); // Defensive fallback: if anything is still hidden after 1.5s, reveal it. const fallback = setTimeout(() => { document .querySelectorAll(".reveal:not(.in), .text-reveal:not(.in)") .forEach((el) => el.classList.add("in")); }, 1500); return () => { io.disconnect(); mo.disconnect(); clearTimeout(fallback); }; }, []); } /* ============================================================ TextReveal — splits text into words for stagger animation Handles string, number, array, and nested React-element children. ============================================================ */ function extractText(node) { if (node == null || typeof node === "boolean") return ""; if (typeof node === "string" || typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(extractText).join(""); if (node.props && node.props.children !== undefined) return extractText(node.props.children); return ""; } function TextReveal({ children, as: Tag = "span", className = "", delay = 0 }) { const text = extractText(children); if (!text.trim()) { // No extractable text — fall back to a plain reveal wrapper return ( {children} ); } const words = text.split(/\s+/); return ( {words.map((w, i) => ( {w} {i < words.length - 1 ? " " : ""} ))} ); } /* ============================================================ Navbar ============================================================ */ function Navbar({ route, setRoute }) { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const f = () => setScrolled(window.scrollY > 24); f(); window.addEventListener("scroll", f, { passive: true }); return () => window.removeEventListener("scroll", f); }, []); const links = [ { id: "home", label: "Home" }, { id: "services", label: "Services" }, { id: "vision", label: "Vision" }, { id: "about", label: "About" }, { id: "team", label: "Team" }, { id: "careers", label: "Careers" }, { id: "partners", label: "Partners" }, { id: "faq", label: "FAQ" }, { id: "contact", label: "Contact" }, ]; const go = (id) => { setRoute(id); setOpen(false); window.scrollTo({ top: 0, behavior: "smooth" }); }; return ( ); } /* ============================================================ Marquee ============================================================ */ function Marquee({ items, dot = true }) { const group = (key) => (
{items.map((it, i) => ( {it} {dot ? : ·} ))}
); return (
{group("a")} {group("b")}
); } /* ============================================================ Placeholder image ============================================================ */ function Ph({ tag, kind = "" }) { return
{tag || "image"}
; } /* ============================================================ Img — real photo with cover-fit, lazy loading ============================================================ */ function Img({ src, alt = "", style = {}, position = "center" }) { const [errored, setErrored] = React.useState(false); if (errored || !src) { return
{alt}
; } return ( {alt} setErrored(true)} style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: position, display: "block", ...style, }} /> ); } /* Curated photo bank — every key resolves to a distinct local file. */ const PHOTOS = { // === Product photos (12 + 2 misc, all distinct) === cards: "assets/products/gloss-card.png", idcard: "assets/products/idcard.webp", envelope: "assets/products/envelop.png", pamphlet: "assets/products/pamplate.png", digitalpaper: "assets/products/digitalpaper.png", rollup: "assets/products/rollup.jpg", canopy: "assets/products/canopy.png", sunpack: "assets/products/sunpack.webp", stickers: "assets/products/stickers.webp", pen: "assets/products/pen.png", medical: "assets/products/medical.jpg", sample: "assets/products/sample.jpg", hero: "assets/hero-img-1.png", features: "assets/features-1.png", // === Environment / category aliases === // Each alias points to a DIFFERENT file than the others used in the same section. press: "assets/hero-img-1.png", // hero only pressFloor: "assets/features-1.png", // culture #1 inkRollers: "assets/products/sample.jpg", // culture #2 meeting: "assets/team/Umesh.png", // culture #3 team: "assets/team/Bhanu.png", // culture #4 colorSwatch: "assets/products/sample.jpg", // press kit #1 paperStack: "assets/products/digitalpaper.png", // press kit #2 (pressFloor reused here, but uniquely within kit) founder: "assets/team/PratapSingh.png", // press kit #5 + about-founder brochure: "assets/products/medical.jpg", // featured + catalog brochure packaging: "assets/products/sunpack.webp", // featured + catalog packaging poster: "assets/features-1.png", // catalog pamphlets // Unused — kept for safety warehouse: "assets/products/canopy.png", delivery: "assets/products/rollup.jpg", designer: "assets/team/shivam.png", bookbinding: "assets/products/idcard.webp", }; /* ============================================================ Counter (animates when in view) ============================================================ */ function Counter({ to, suffix = "", duration = 1400 }) { const ref = useRef(null); const [val, setVal] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; let raf; const io = new IntersectionObserver((entries) => { for (const e of entries) { if (e.isIntersecting) { const start = performance.now(); const step = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(to * eased)); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); io.unobserve(el); } } }); io.observe(el); return () => { io.disconnect(); cancelAnimationFrame(raf); }; }, [to, duration]); return ( {val.toLocaleString("en-IN")} {suffix} ); } /* ============================================================ Section head ============================================================ */ function SectionHead({ eyebrow, title, sub, align = "split" }) { if (align === "center") { return (
{eyebrow &&
{eyebrow}
}

{title}

{sub &&

{sub}

}
); } return (
{eyebrow &&
{eyebrow}
} {title}
{sub &&

{sub}

}
); } /* ============================================================ Footer ============================================================ */ function Footer({ setRoute }) { const link = (id, label) => ( { e.preventDefault(); setRoute(id); window.scrollTo({ top: 0, behavior: "smooth" }); }}> {label} ); return ( ); } /* ============================================================ Cursor blob effect ============================================================ */ function useCursorBlob() { useEffect(() => { const el = document.getElementById("cursor"); if (!el) return; let x = 0, y = 0, tx = 0, ty = 0, raf; const move = (e) => { tx = e.clientX; ty = e.clientY; el.style.opacity = "0.85"; }; const leave = () => { el.style.opacity = "0"; }; const enterLink = () => { el.style.width = "60px"; el.style.height = "60px"; }; const leaveLink = () => { el.style.width = "28px"; el.style.height = "28px"; }; window.addEventListener("mousemove", move); window.addEventListener("mouseleave", leave); const links = () => document.querySelectorAll("a, button, .product-card, .service-card, .job, .team-card"); const refresh = () => { links().forEach((l) => { l.addEventListener("mouseenter", enterLink); l.addEventListener("mouseleave", leaveLink); }); }; refresh(); const mo = new MutationObserver(refresh); mo.observe(document.body, { childList: true, subtree: true }); const loop = () => { x += (tx - x) * 0.18; y += (ty - y) * 0.18; el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; raf = requestAnimationFrame(loop); }; loop(); return () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseleave", leave); mo.disconnect(); cancelAnimationFrame(raf); }; }, []); } /* ============================================================ Sticky scroll sequence ============================================================ */ function StickyScroll({ steps, render }) { const [active, setActive] = useState(0); const refs = useRef([]); useEffect(() => { const observers = refs.current.map((el, i) => { const io = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) setActive(i); } }, { threshold: 0.6 } ); if (el) io.observe(el); return io; }); return () => observers.forEach((o) => o.disconnect()); }, [steps]); return (
{steps.map((s, i) => (
{render(s, i, active === i)}
))}
{steps.map((s, i) => (
(refs.current[i] = el)} className={`sticky-step ${active === i ? "active" : ""}`} onClick={() => setActive(i)} >
Step {String(i + 1).padStart(2, "0")} / {String(steps.length).padStart(2, "0")}

{s.title}

{s.body}

))}
); } Object.assign(window, { useScrollReveal, useCursorBlob, TextReveal, Navbar, Marquee, Ph, Img, PHOTOS, Counter, SectionHead, Footer, StickyScroll, });