/* ring.jsx — cinematic 3D project carousel + grid view + filter overlay Redesigned: section header sits ABOVE the ring (no text over cards); a live caption bar tracks the front-most card; category quick-filters replace the scattered labels; arrows + drag + hover + keyboard all drive it. */ const { useState: uS, useEffect: uE, useRef: uR, useMemo: uM, useCallback: uC } = React; const CAT_ICON = { villa: "building", hospital: "hospital", commercial: "layers", infrastructure: "ruler", interior: "sofa", farmhouse: "trees", landscape: "leaf", residential: "building" }; function statusTone(s) { return s === "delivered" ? "success" : s === "ongoing" ? "warning" : "gold"; } /* ============================ the 3D carousel =========================== */ /* Robust index-based 3D coverflow — deterministic front card, momentum drag, snap, keyboard, trackpad, subtle mouse-parallax tilt, gentle auto-advance. */ function Ring({ items, lang, activeId, setActive, onFront, onCardOpen }) { const stageRef = uR(null), innerRef = uR(null), cardRefs = uR([]); const S = uR({ pos: 0, target: 0, dragging: false, hover: false, lastX: 0, moved: 0, tpx: 0, tpy: 0, px: 0, py: 0, front: -1, reduce: false, lastStep: 0 }); const onFrontRef = uR(onFront); onFrontRef.current = onFront; const openRef = uR(onCardOpen); openRef.current = onCardOpen; const [dims, setDims] = uS(null); cardRefs.current = []; const N = items.length; const nearestDelta = (i) => { let d = ((i - Math.round(S.current.pos)) % N + N) % N; if (d > N / 2) d -= N; return d; }; const goTo = (i) => { S.current.target = Math.round(S.current.pos) + nearestDelta(i); }; const step = (dir) => { S.current.target = Math.round(S.current.pos) + dir; S.current.lastStep = performance.now(); }; /* dimensions */ uE(() => { S.current.reduce = window.matchMedia("(prefers-reduced-motion:reduce)").matches; const stage = stageRef.current; if (!stage) return; const measure = () => { const w = stage.clientWidth; const cardW = Math.max(300, Math.min(w * 0.38, 520)); setDims({ w, cardW, cardH: cardW * 0.66, spacing: cardW * (w < 720 ? 0.46 : 0.52), depth: Math.min(w * 0.24, 280), angle: w < 720 ? 40 : 46, persp: 1700 }); }; measure(); const ro = new ResizeObserver(measure); ro.observe(stage); return () => ro.disconnect(); }, []); /* external activeId (audience picks) -> center that card */ uE(() => { if (activeId == null) return; const idx = items.findIndex((p) => p.id === activeId); if (idx >= 0) goTo(idx); }, [activeId, items]); /* animation loop */ uE(() => { if (!dims || !N) return; let raf; const s = S.current; const frame = (t) => { if (!s.dragging) { s.pos += (s.target - s.pos) * 0.14; if (Math.abs(s.target - s.pos) < 0.0008) s.pos = s.target; } s.px += (s.tpx - s.px) * 0.05; s.py += (s.tpy - s.py) * 0.05; const inner = innerRef.current; if (inner) inner.style.transform = `rotateX(${(-s.py * 5).toFixed(2)}deg) rotateY(${(s.px * 6).toFixed(2)}deg)`; for (let i = 0; i < N; i++) { const el = cardRefs.current[i]; if (!el) continue; let off = ((i - s.pos) % N + N) % N; if (off > N / 2) off -= N; const a = Math.abs(off); const x = off * dims.spacing; const z = -a * dims.depth; const ry = Math.max(-60, Math.min(60, -off * dims.angle)); const scale = Math.max(0.6, 1 - a * 0.14); const op = a > 3.1 ? 0 : Math.max(0, 1 - a * 0.30); const blur = Math.min(5, a * 1.5); el.style.transform = `translate(-50%,-50%) translate3d(${x.toFixed(1)}px,0,${z.toFixed(1)}px) rotateY(${ry.toFixed(1)}deg) scale(${scale.toFixed(3)})`; el.style.opacity = op.toFixed(3); el.style.filter = blur > 0.2 ? `blur(${blur.toFixed(1)}px)` : "none"; el.style.zIndex = String(1000 - Math.round(a * 100)); el.style.pointerEvents = op > 0.35 ? "auto" : "none"; } const front = ((Math.round(s.pos) % N) + N) % N; if (front !== s.front) { const p = cardRefs.current[s.front]; if (p) p.classList.remove("front"); const c = cardRefs.current[front]; if (c) c.classList.add("front"); s.front = front; if (items[front] && onFrontRef.current) onFrontRef.current(items[front].id); } const idle = s.pos === s.target && !s.hover && !s.dragging; if (!s.reduce && idle && t - s.lastStep > 4600) { s.target += 1; s.lastStep = t; } else if (!idle) { s.lastStep = t; } raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); }, [dims, items]); /* drag (pointer events, direction-locked) + trackpad + mouse-parallax */ uE(() => { if (!dims) return; const stage = stageRef.current, s = S.current; let startX = 0, startY = 0, decided = null, pid = null; const down = (e) => { if (e.pointerType === "mouse" && e.button !== 0) return; pid = e.pointerId; startX = e.clientX; startY = e.clientY; decided = null; s.lastX = e.clientX; s.moved = 0; s.dragging = false; s.target = s.pos; s.lastStep = performance.now(); // freeze auto-advance/settle at current spot }; const move = (e) => { if (pid !== null && e.pointerId !== pid) return; if (decided === null) { const adx = Math.abs(e.clientX - startX), ady = Math.abs(e.clientY - startY); if (adx < 6 && ady < 6) return; decided = adx > ady ? "h" : "v"; if (decided === "h") { s.dragging = true; stage.classList.add("grabbing"); try { stage.setPointerCapture(pid); } catch (_) {} } } if (decided !== "h") return; // vertical gesture -> let the page scroll const dx = e.clientX - s.lastX; s.lastX = e.clientX; s.moved += Math.abs(dx); s.pos -= dx / dims.spacing; if (e.cancelable) e.preventDefault(); }; const up = (e) => { if (pid !== null && e.pointerId !== pid) return; pid = null; if (s.dragging) { s.dragging = false; stage.classList.remove("grabbing"); s.target = Math.round(s.pos); } decided = null; }; const enter = () => { s.hover = true; }; const leave = () => { s.hover = false; s.tpx = 0; s.tpy = 0; }; const par = (e) => { if (e.pointerType && e.pointerType !== "mouse") return; const r = stage.getBoundingClientRect(); s.tpx = ((e.clientX - r.left) / r.width - 0.5) * 2; s.tpy = ((e.clientY - r.top) / r.height - 0.5) * 2; }; let wt = 0; const wheel = (e) => { if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return; e.preventDefault(); const now = Date.now(); if (now - wt < 240) return; wt = now; step(e.deltaX > 0 ? 1 : -1); }; stage.addEventListener("pointerdown", down); stage.addEventListener("pointermove", move); stage.addEventListener("pointermove", par); stage.addEventListener("pointerup", up); stage.addEventListener("pointercancel", up); stage.addEventListener("pointerenter", enter); stage.addEventListener("pointerleave", leave); stage.addEventListener("wheel", wheel, { passive: false }); return () => { stage.removeEventListener("pointerdown", down); stage.removeEventListener("pointermove", move); stage.removeEventListener("pointermove", par); stage.removeEventListener("pointerup", up); stage.removeEventListener("pointercancel", up); stage.removeEventListener("pointerenter", enter); stage.removeEventListener("pointerleave", leave); stage.removeEventListener("wheel", wheel); }; }, [dims]); const onKey = (e) => { if (e.key === "ArrowRight") { e.preventDefault(); step(1); } else if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); } else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const f = items[S.current.front]; if (f) openRef.current(f.id); } }; return (
{items.map((p, i) => ( ))}
); } /* ====================== live caption bar ================================ */ function Caption({ project, lang, index, total, onOpen }) { const T = (v) => window.t(v, lang); if (!project) return null; const cat = window.OLV.categories.find((c) => c.id === project.cat); const status = window.OLV.statuses.find((s) => s.id === project.status); return (

{T(project.title)}

{T(project.summary)}

{project.gallery && project.gallery.length > 0 && ( )}
); } /* ====================== project gallery lightbox ======================= */ function Lightbox({ project, lang, onClose }) { const T = (v) => window.t(v, lang); const gallery = (project && project.gallery) || []; const [idx, setIdx] = uS(0); const stripRef = uR(null); const go = uC((d) => setIdx((i) => (i + d + gallery.length) % gallery.length), [gallery.length]); uE(() => { setIdx(0); }, [project && project.id]); uE(() => { if (!project) return; const onKey = (e) => { if (e.key === "Escape") onClose(); else if (e.key === "ArrowRight") go(1); else if (e.key === "ArrowLeft") go(-1); }; window.addEventListener("keydown", onKey); const prevOv = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prevOv; }; }, [project, go, onClose]); uE(() => { const strip = stripRef.current; if (!strip) return; const el = strip.children[idx]; if (el) strip.scrollTo({ left: el.offsetLeft - strip.clientWidth / 2 + el.clientWidth / 2, behavior: "smooth" }); }, [idx]); if (!project) return null; const cat = window.OLV.categories.find((c) => c.id === project.cat); return (
{gallery.map((src, i) => ( ))}
{String(idx + 1).padStart(2, "0")} / {String(gallery.length).padStart(2, "0")}
{cat && T(cat)}

{T(project.title)}

{T(project.loc)}{project.area}

{T(project.desc || project.summary)}

{gallery.map((src, i) => ( ))}
); } /* ====================== portfolio section ============================== */ function Portfolio({ lang }) { const T = (v) => window.t(v, lang); const P = window.OLV.portfolio; const [activeId, setActive] = uS(null); const [frontId, setFrontId] = uS(null); const [lightboxId, setLightbox] = uS(null); const items = window.OLV.projects; const captionProj = uM(() => items.find((p) => p.id === frontId) || items[0] || null, [frontId, items]); const frontIndex = captionProj ? items.findIndex((p) => p.id === captionProj.id) : -1; const lightboxProj = uM(() => items.find((p) => p.id === lightboxId) || null, [lightboxId, items]); /* audience cards dispatch a category -> rotate to first matching project */ uE(() => { const onFilter = (e) => { const cat = e.detail; const m = items.find((p) => p.cat === cat); if (m) setActive(m.id); }; window.addEventListener("olv-filter-cat", onFilter); return () => window.removeEventListener("olv-filter-cat", onFilter); }, []); return (
{T(P.overline)}

{T(P.sub)}

{T({ en: "Drag · hover · click a project to view its gallery", hi: "खींचें · होवर · गैलरी हेतु प्रोजेक्ट पर क्लिक करें" })}
{items.length > 0 &&
{String(frontIndex + 1).padStart(2, "0")} / {String(items.length).padStart(2, "0")}
} {!items.length &&
{T(P.empty)}
}
setLightbox(null)} />
); } Object.assign(window, { Portfolio });