/* Interactive 3D elliptical ring hero — Nexgen Solutions Chitral. Vanilla ellipse math + RAF; panels positioned imperatively for 60fps. Depends on: window.NEXGEN_ITEMS, window.NexgenSolutionsChitralDesignSystem_3ef89c */ const { AccentLine: RAccentLine } = window.NexgenSolutionsChitralDesignSystem_3ef89c; function NexgenRing() { const items = window.NEXGEN_ITEMS; const REPEAT = 2; const panels = React.useMemo(() => { const out = []; for (let r = 0; r < REPEAT; r++) items.forEach((it, i) => out.push({ ...it, dataIdx: i })); return out; }, [items]); const N = panels.length; const reduced = React.useRef(typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches).current; const stageRef = React.useRef(null); const cellRefs = React.useRef([]); const cursorRef = React.useRef(null); const rot = React.useRef(0); const vel = React.useRef(0); const target = React.useRef(null); // locked rotation target (click) const tilt = React.useRef(0); // current vertical tilt (mouseY) const tiltTarget = React.useRef(0); const yaw = React.useRef(0); // subtle horizontal offset (mouseX) const yawTarget = React.useRef(0); const dims = React.useRef({ w: 1200, rx: 620, ry: 168, cx: 600, cy: 300 }); const dragging = React.useRef(false); const lastX = React.useRef(0); const hoverPanel = React.useRef(-1); const cur = React.useRef({ x: 0, y: 0 }); // cursor pos const curLerp = React.useRef({ x: 0, y: 0 }); const [display, setDisplay] = React.useState(null); // item shown in center (hover) const [selected, setSelected] = React.useState(null); // { dataIdx } const [cursorActive, setCursorActive] = React.useState(false); const AUTO = reduced ? 0 : 0.0016; function measure() { const el = stageRef.current; if (!el) return; const w = el.clientWidth, h = el.clientHeight; const rx = Math.min(w * 0.44, 720); const ry = Math.max(120, Math.min(rx * 0.28, 210)); dims.current = { w, h, rx, ry, cx: w / 2, cy: h * 0.5 }; } React.useEffect(() => { measure(); const ro = new ResizeObserver(measure); if (stageRef.current) ro.observe(stageRef.current); let raf; const frame = () => { const d = dims.current; // rotation if (target.current != null && !dragging.current) { rot.current += (target.current - rot.current) * 0.08; } else { rot.current += vel.current; vel.current *= 0.93; if (Math.abs(vel.current) < 0.00002) vel.current = 0; if (target.current == null && !dragging.current) rot.current += AUTO; } // tilt + yaw ease tilt.current += (tiltTarget.current - tilt.current) * 0.06; yaw.current += (yawTarget.current - yaw.current) * 0.06; const ryEff = d.ry * (1 + tilt.current * 0.5); for (let i = 0; i < N; i++) { const cell = cellRefs.current[i]; if (!cell) continue; const angle = (i / N) * Math.PI * 2 + rot.current + Math.PI / 2; const x = Math.cos(angle) * d.rx + yaw.current * 26; const depth = Math.sin(angle); // -1 back(top) .. +1 front(bottom) const y = depth * ryEff; const t = (depth + 1) / 2; // 0 back .. 1 front const scale = 0.5 + t * 0.62; // 0.5 -> 1.12 const op = 0.22 + t * 0.78; const bright = 0.55 + t * 0.55; const ry2 = -(x / d.rx) * 20; // face toward center const z = Math.round(t * 1000); const isHover = hoverPanel.current === i; const hoverBoost = isHover ? 1.14 : 1; cell.style.transform = `translate3d(${d.cx + x}px, ${d.cy + y}px, 0) translate(-50%,-50%) rotateY(${ry2}deg) scale(${scale * hoverBoost})`; cell.style.opacity = isHover ? Math.min(1, op + 0.2) : op; cell.style.zIndex = isHover ? 1200 : z; cell.style.filter = `brightness(${isHover ? Math.min(1.2, bright + 0.35) : bright}) blur(${t < 0.35 ? (0.35 - t) * 3 : 0}px)`; } // cursor follower if (cursorRef.current) { curLerp.current.x += (cur.current.x - curLerp.current.x) * 0.2; curLerp.current.y += (cur.current.y - curLerp.current.y) * 0.2; cursorRef.current.style.transform = `translate3d(${curLerp.current.x}px, ${curLerp.current.y}px, 0) translate(-50%,-50%)`; } raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, [N]); // pointer / mouse handlers on the stage const onMove = (e) => { const d = dims.current; const rect = stageRef.current.getBoundingClientRect(); cur.current.x = e.clientX - rect.left; cur.current.y = e.clientY - rect.top; if (reduced) return; const nx = (e.clientX - rect.left) / rect.width - 0.5; // -0.5..0.5 const ny = (e.clientY - rect.top) / rect.height - 0.5; yawTarget.current = nx * 1.2; tiltTarget.current = -ny * 0.5; if (dragging.current) { const dx = e.clientX - lastX.current; lastX.current = e.clientX; vel.current += (dx / rect.width) * 0.6; target.current = null; } }; const onDown = (e) => { dragging.current = true; lastX.current = e.clientX; target.current = null; setSelected(null); stageRef.current.setPointerCapture?.(e.pointerId); }; const onUp = () => { dragging.current = false; }; const enterPanel = (i, item) => { hoverPanel.current = i; setDisplay(item); setCursorActive(true); }; const leavePanel = () => { hoverPanel.current = -1; setCursorActive(false); setDisplay(selected != null ? items[selected] : null); }; const clickPanel = (i, item) => { // rotate this panel to front-center: angle_i + rot + π/2 == π/2 => rot = -angle_i const base = -((i / N) * Math.PI * 2); const k = Math.round((rot.current - base) / (Math.PI * 2)); target.current = base + k * Math.PI * 2; setSelected(item.dataIdx); setDisplay(item); }; const shown = display; const locked = selected != null && display && items[selected] && display.id === items[selected].id; const ctaHref = (cta) => { const t = (cta || "").toUpperCase(); if (t.includes("COURSE")) return "courses.html"; if (t.includes("PROJECT")) return "index.html#projects"; if (t.includes("CONTACT")) return "contact.html"; if (t.includes("ABOUT")) return "about.html"; return "services.html"; }; return (
{/* editorial category markers */} {/* the ring stage */}
{ onUp(); leavePanel(); cur.current.x = -200; }} style={{ position: "absolute", inset: 0, perspective: "1300px", cursor: reduced ? "default" : "none", touchAction: "pan-y" }} >
{panels.map((p, i) => (
(cellRefs.current[i] = el)} className="ring-cell" onPointerEnter={() => enterPanel(i, p)} onPointerLeave={leavePanel} onClick={() => clickPanel(i, p)} style={{ position: "absolute", left: 0, top: 0, width: 46, height: 116, willChange: "transform, opacity, filter" }} >
{p.title} { e.currentTarget.style.opacity = 0; }} />
))}
{/* custom cursor follower */} {!reduced && ( )}
{/* center content */}
{shown ? shown.category : "Nexgen Solutions Chitral"}
{shown ? ( <>

{shown.title}

{shown.desc}

{shown.cta} → {!locked && Click to lock · drag to rotate}
) : ( <>

Build what's Next

We build digital solutions, create powerful visuals, integrate AI, grow businesses, and teach the skills of tomorrow.

Explore services → Start learning →
)}
MOVE TO EXPLORE · CLICK TO DISCOVER
); } window.NexgenRing = NexgenRing;