import React, { useEffect, useRef, useState } from "react";
/* ===== Styles (design tokens → components), scoped under .vx ===== */
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@600;700&family=Inter:wght@400;500;600&display=swap');
.vx{
--violet-500:#8b5cf6; --cyan-400:#22d3ee; --green-400:#4ade80; --ink-950:#07060d;
--color-bg:var(--ink-950); --color-text:#f4f3ff; --color-muted:#a5a1c2;
--color-primary:var(--violet-500); --color-accent:var(--cyan-400);
--color-border:rgba(255,255,255,.1); --color-surface:rgba(18,15,32,.82);
--radius-card:20px; --ease:cubic-bezier(.2,.8,.2,1);
--font-display:'Chakra Petch',system-ui,sans-serif; --font-body:'Inter',system-ui,sans-serif;
--btn-height:54px; --btn-pad-x:2.25rem; --btn-radius:999px; --dur:.2s;
--btn-primary-top:#6d4aff; --btn-primary-bottom:#4a30d4; --btn-primary-glow:rgba(109,74,255,.75);
--btn-highlight:rgba(255,255,255,.38); --btn-border:rgba(255,255,255,.26); --btn-border-hover:rgba(255,255,255,.55);
--btn-ghost-top:rgba(255,255,255,.14); --btn-ghost-bottom:rgba(255,255,255,.04);
position:relative;min-height:100svh;background:var(--color-bg);color:var(--color-text);
font-family:var(--font-body);overflow-x:hidden;box-sizing:border-box;color-scheme:dark;
}
.vx *{box-sizing:border-box}
.vx-bg{position:fixed;inset:0;width:100%;height:100%;z-index:0;pointer-events:none}
.vx-hero{position:relative;z-index:1;min-height:100svh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:calc(env(safe-area-inset-top,0px) + 40px) 20px calc(env(safe-area-inset-bottom,0px) + 40px)}
.vx-badge{display:inline-flex;align-items:center;gap:.6rem;padding:.5rem 1rem;border-radius:999px;background:var(--color-surface);border:1px solid var(--color-border);color:var(--color-text);letter-spacing:.14em;font-size:.8rem;font-weight:600;margin-bottom:1.75rem;animation:vx-rise .6s var(--ease) both}
.vx-dot{width:8px;height:8px;border-radius:50%;background:var(--green-400);box-shadow:0 0 10px var(--green-400);animation:vx-pulse 1.8s infinite}
.vx h1{font-family:var(--font-display);font-weight:700;line-height:1.04;letter-spacing:-.01em;margin:0 0 1.25rem;font-size:clamp(2.5rem,8.2vw,5.4rem);animation:vx-rise .6s .08s var(--ease) both}
.vx-grad{background:linear-gradient(100deg,var(--color-accent),var(--color-primary) 55%,#f0abfc);-webkit-background-clip:text;background-clip:text;color:transparent}
.vx-sub{max-width:36rem;margin:0 0 2.25rem;color:var(--color-muted);font-size:clamp(1rem,2.2vw,1.2rem);line-height:1.65;animation:vx-rise .6s .16s var(--ease) both}
.vx-actions{display:flex;gap:.9rem;flex-wrap:wrap;justify-content:center;margin-bottom:3.5rem;animation:vx-rise .6s .24s var(--ease) both}
.vx-btn{position:relative;overflow:hidden;isolation:isolate;display:inline-flex;align-items:center;justify-content:center;min-height:var(--btn-height);padding:0 var(--btn-pad-x);border-radius:var(--btn-radius);font:600 .95rem var(--font-body);letter-spacing:.04em;color:var(--color-text);text-decoration:none;cursor:pointer;border:1px solid var(--btn-border);backdrop-filter:blur(14px) saturate(1.5);-webkit-backdrop-filter:blur(14px) saturate(1.5);transition:transform var(--dur) var(--ease),box-shadow .3s var(--ease),border-color var(--dur)}
.vx-btn::before{content:"";position:absolute;inset:1px 1px 52% 1px;border-radius:var(--btn-radius) var(--btn-radius) 50% 50%/var(--btn-radius) var(--btn-radius) 100% 100%;background:linear-gradient(180deg,rgba(255,255,255,.2),rgba(255,255,255,0));z-index:-1;pointer-events:none}
.vx-btn::after{content:"";position:absolute;top:0;left:-70%;width:34%;height:100%;background:linear-gradient(105deg,transparent,rgba(255,255,255,.25),transparent);transform:skewX(-20deg);transition:left .8s var(--ease);z-index:-1;pointer-events:none}
.vx-btn:hover::after{left:150%}
.vx-btn:active{transform:scale(.98)}
.vx-btn-primary{background:linear-gradient(180deg,var(--btn-primary-top),var(--btn-primary-bottom));box-shadow:inset 0 1px 0 var(--btn-highlight),0 12px 30px -10px var(--btn-primary-glow)}
.vx-btn-primary:hover{transform:translateY(-1px);border-color:var(--btn-border-hover);box-shadow:inset 0 1px 0 var(--btn-highlight),0 16px 36px -10px var(--btn-primary-glow)}
.vx-btn-ghost{background:linear-gradient(180deg,var(--btn-ghost-top),var(--btn-ghost-bottom));box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 10px 26px -12px rgba(0,0,0,.8)}
.vx-btn-ghost:hover{transform:translateY(-1px);border-color:var(--btn-border-hover);background:linear-gradient(180deg,rgba(255,255,255,.2),rgba(255,255,255,.07))}
.vx :focus-visible{outline:2px solid var(--color-accent);outline-offset:3px}
.vx-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:.8rem;width:100%;max-width:42rem;margin:0;animation:vx-rise .6s .32s var(--ease) both}
.vx-stat{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-card);padding:1.1rem .5rem;transition:transform .26s var(--ease),border-color .26s}
.vx-stat:hover{transform:translateY(-3px);border-color:rgba(139,92,246,.5)}
.vx-stat dd{margin:0;font:700 clamp(1.6rem,4vw,2.1rem)/1 var(--font-display)}
.vx-stat dt{margin-top:.45rem;font-size:clamp(.68rem,1.6vw,.82rem);color:var(--color-muted)}
@keyframes vx-rise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
@keyframes vx-pulse{50%{opacity:.4}}
@media (prefers-reduced-motion:reduce){.vx *,.vx *::before,.vx *::after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
`;
/* ===== Hooks ===== */
const prefersReducedMotion = (): boolean =>
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/** Perspective grid background with soft fade at the horizon. */
function useGridBackground(canvasRef: React.RefObject<HTMLCanvasElement>): void {
useEffect(() => {
const cv = canvasRef.current;
const cx = cv?.getContext("2d");
if (!cv || !cx) return;
const still = prefersReducedMotion();
let w = 0, h = 0, t = 0, raf = 0;
const fit = () => {
const d = window.devicePixelRatio || 1;
w = window.innerWidth; h = window.innerHeight;
cv.width = w * d; cv.height = h * d;
cx.setTransform(d, 0, 0, d, 0, 0);
};
const draw = () => {
cx.clearRect(0, 0, w, h);
const glow = cx.createRadialGradient(w / 2, h * 0.08, 0, w / 2, h * 0.08, h * 0.85);
glow.addColorStop(0, "rgba(139,92,246,.30)");
glow.addColorStop(0.55, "rgba(34,211,238,.05)");
glow.addColorStop(1, "rgba(7,6,13,0)");
cx.fillStyle = glow; cx.fillRect(0, 0, w, h);
const hor = h * 0.66;
cx.lineWidth = 1;
const fade = cx.createLinearGradient(0, hor, 0, h);
fade.addColorStop(0, "rgba(139,92,246,0)");
fade.addColorStop(0.35, "rgba(139,92,246,.16)");
fade.addColorStop(1, "rgba(139,92,246,.34)");
cx.strokeStyle = fade;
for (let i = -16; i <= 16; i++) {
cx.beginPath(); cx.moveTo(w / 2 + i * 22, hor); cx.lineTo(w / 2 + i * w * 0.12, h); cx.stroke();
}
for (let i = 0; i < 14; i++) {
const p = ((i + t) % 14) / 14;
const y = hor + Math.pow(p, 2.4) * (h - hor);
cx.strokeStyle = `rgba(34,211,238,${Math.pow(p, 1.4) * 0.26})`;
cx.beginPath(); cx.moveTo(0, y); cx.lineTo(w, y); cx.stroke();
}
const v = cx.createRadialGradient(w / 2, h / 2, h * 0.35, w / 2, h / 2, h * 0.95);
v.addColorStop(0, "rgba(7,6,13,0)"); v.addColorStop(1, "rgba(7,6,13,.75)");
cx.fillStyle = v; cx.fillRect(0, 0, w, h);
if (!still) { t += 0.018; raf = requestAnimationFrame(draw); }
};
const onResize = () => { fit(); if (still) draw(); };
fit(); draw();
window.addEventListener("resize", onResize);
return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", onResize); };
}, [canvasRef]);
}
/** Eased count-up from 0 to target. */
function useCountUp(target: number, frames = 40): number {
const [value, setValue] = useState(0);
useEffect(() => {
if (prefersReducedMotion()) { setValue(target); return; }
let f = 0, id = 0;
const step = () => {
f++;
setValue(Math.round(target * (1 - Math.pow(1 - f / frames, 3))));
if (f < frames) id = requestAnimationFrame(step);
};
step();
return () => cancelAnimationFrame(id);
}, [target, frames]);
return value;
}
/* ===== Components ===== */
const GamepadIcon: React.FC<{ size?: number }> = ({ size = 18 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth={1.9} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M6 12h4M8 10v4M15 13h.01M18 11h.01M17.3 5H6.7a4 4 0 0 0-3.9 3.1L1.6 14a3 3 0 0 0 5.2 2.5L8 15h8l1.2 1.5a3 3 0 0 0 5.2-2.5l-1.2-5.9A4 4 0 0 0 17.3 5z" />
</svg>
);
interface StatProps { value: number; suffix: string; label: string; }
const Stat: React.FC<StatProps> = ({ value, suffix, label }) => {
const n = useCountUp(value);
return (
<div className="vx-stat">
<dd className="vx-grad">{n}{suffix}</dd>
<dt>{label}</dt>
</div>
);
};
export interface VortexHeroProps {
onExplore?: () => void;
onLearnMore?: () => void;
}
const VortexHero: React.FC<VortexHeroProps> = ({ onExplore, onLearnMore }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useGridBackground(canvasRef);
return (
<div className="vx">
<style>{CSS}</style>
<canvas ref={canvasRef} className="vx-bg" aria-hidden="true" />
<section className="vx-hero">
<div className="vx-badge">
<span className="vx-dot" />
<GamepadIcon />
COMING SOON
</div>
<h1>
Enter the Arena,<br />
<span className="vx-grad">Become a Legend</span>
</h1>
<p className="vx-sub">
One gateway to the best competitive, RPG, and racing games. Search, pick, and start playing.
</p>
<div className="vx-actions">
<button type="button" className="vx-btn vx-btn-primary" onClick={onExplore}>Explore Games</button>
<button type="button" className="vx-btn vx-btn-ghost" onClick={onLearnMore}>Why VORTEX?</button>
</div>
<dl className="vx-stats">
<Stat value={6} suffix="+" label="Games Available" />
<Stat value={4} suffix="+" label="Genres" />
<Stat value={24} suffix="/7" label="Servers Online" />
</dl>
</section>
</div>
);
};
export default VortexHero;