Initial commit: UP Parivaar Dallas site
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// Selectors for elements that should animate in on scroll (scoped to <main>).
|
||||
const TARGETS = [
|
||||
"h1", "h2", "h3", "h4",
|
||||
".eyebrow", ".eyebrow-light",
|
||||
"p",
|
||||
".btn-primary", ".btn-ghost", ".link-arrow", ".chip",
|
||||
'[class*="card"]', '[class*="__tile"]', '[class*="__item"]',
|
||||
'[class*="__pillar"]', '[class*="__step"]', '[class*="__cat"]',
|
||||
]
|
||||
.map((s) => `main ${s}`)
|
||||
.join(",");
|
||||
|
||||
// Sections that manage their own motion / must not be hidden.
|
||||
const EXCLUDE = ".home-hero, .jbanner, .nav, .footer, nav, footer, header";
|
||||
|
||||
export default function ScrollReveal() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
let cleanup = () => {};
|
||||
const id = window.requestAnimationFrame(() => {
|
||||
const all = Array.from(document.querySelectorAll<HTMLElement>(TARGETS)).filter(
|
||||
(el) => !el.closest(EXCLUDE)
|
||||
);
|
||||
|
||||
// Keep only the outermost targets so a card animates as one unit
|
||||
// (its inner heading/paragraph don't animate separately).
|
||||
const set = new Set(all);
|
||||
const targets = all.filter((el) => {
|
||||
let p = el.parentElement;
|
||||
while (p) {
|
||||
if (set.has(p)) return false;
|
||||
p = p.parentElement;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Small stagger between siblings that share a parent.
|
||||
const byParent = new Map<Element, HTMLElement[]>();
|
||||
targets.forEach((el) => {
|
||||
const parent = el.parentElement;
|
||||
if (!parent) return;
|
||||
const arr = byParent.get(parent) ?? [];
|
||||
arr.push(el);
|
||||
byParent.set(parent, arr);
|
||||
});
|
||||
byParent.forEach((arr) => {
|
||||
arr.forEach((el, idx) => {
|
||||
el.style.setProperty("--reveal-delay", `${Math.min(idx * 0.08, 0.4)}s`);
|
||||
});
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "0px 0px -8% 0px", threshold: 0.08 }
|
||||
);
|
||||
|
||||
const variantFor = (el: HTMLElement) => {
|
||||
const cls = el.className || "";
|
||||
if (el.matches(".eyebrow, .eyebrow-light")) return "reveal--left";
|
||||
if (
|
||||
/card|__tile|__item|__pillar|__step|__cat/.test(
|
||||
typeof cls === "string" ? cls : ""
|
||||
)
|
||||
) {
|
||||
return "reveal--zoom";
|
||||
}
|
||||
return "reveal--up";
|
||||
};
|
||||
|
||||
targets.forEach((el) => {
|
||||
el.classList.add("reveal", variantFor(el));
|
||||
const rect = el.getBoundingClientRect();
|
||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
if (inView) {
|
||||
// Already visible on load — reveal instantly, no hidden flash.
|
||||
el.classList.add("is-visible");
|
||||
} else {
|
||||
observer.observe(el);
|
||||
}
|
||||
});
|
||||
|
||||
cleanup = () => observer.disconnect();
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(id);
|
||||
cleanup();
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user