/* 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 (
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,
});