"use client";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import MilkyWay from "@/components/ui/milky-way";
/* ------------------------------------------------------------------ *
* Small rotating "explore" badge — spins continuously, speeds up on
* hover. Presentational chrome for this demo, not part of the effect.
* ------------------------------------------------------------------ */
function CircularText({
text = "",
spinDuration = 20,
radius = 64,
}: {
text?: string;
spinDuration?: number;
radius?: number;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const tweenRef = useRef<gsap.core.Tween | null>(null);
const letters = Array.from(text);
useEffect(() => {
if (!containerRef.current) return;
tweenRef.current?.kill();
if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) {
return;
}
tweenRef.current = gsap.to(containerRef.current, {
rotation: 360,
duration: spinDuration,
ease: "none",
repeat: -1,
transformOrigin: "50% 50%",
});
return () => {
tweenRef.current?.kill();
};
}, [spinDuration, text]);
const handleEnter = () => {
if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return;
if (!tweenRef.current) return;
gsap.to(tweenRef.current, { timeScale: 2, duration: 0.3 });
};
const handleLeave = () => {
if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return;
if (!tweenRef.current) return;
tweenRef.current.resume();
gsap.to(tweenRef.current, { timeScale: 1, duration: 0.3 });
};
return (
<div style={{ position: "relative" }}>
<span
style={{
position: "absolute",
left: "50%",
top: "50%",
width: 44,
height: 44,
transform: "translate(-50%, -50%)",
zIndex: 2,
userSelect: "none",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width={22} height={22} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z" />
</svg>
</span>
<div
ref={containerRef}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
style={{ width: radius * 2 + 20, height: radius * 2 + 20, position: "relative" }}
>
{letters.map((letter, i) => {
const angle = (360 / letters.length) * i;
return (
<span
key={i}
style={{
position: "absolute",
left: "50%",
top: "50%",
transform: `rotate(${angle}deg) translate(${radius}px) rotate(90deg)`,
transformOrigin: "0 0",
fontSize: 15,
fontWeight: 600,
whiteSpace: "pre",
pointerEvents: "none",
zIndex: 3,
}}
>
{letter}
</span>
);
})}
</div>
</div>
);
}
export default function MilkyWayDemo() {
// Hide the spinning badge on touch devices — the component itself swaps to
// a plain "open on desktop" message there, and there's no cursor to invite.
const [isCoarsePointer, setIsCoarsePointer] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(pointer: coarse)");
const update = () => setIsCoarsePointer(mq.matches);
update();
mq.addEventListener("change", update);
return () => mq.removeEventListener("change", update);
}, []);
return (
<div className="relative h-screen w-full overflow-hidden bg-black">
<MilkyWay particleSize={0.6} rotationSpeed={0.35} mouseInfluence />
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 flex items-end justify-between gap-6 p-[2vw] text-white">
<div className="max-w-sm">
<p className="text-xs uppercase tracking-[0.2em] text-white/50">
WebGL · GPGPU particles
</p>
<h1 className="mt-1 text-2xl font-medium sm:text-3xl">Milky Way</h1>
<p className="mt-1 text-sm leading-relaxed text-white/60">
A GPU-simulated spiral galaxy with drifting nebula smoke and a
starfield backdrop — tilts toward the cursor.
</p>
</div>
{!isCoarsePointer && (
<div className="pointer-events-auto shrink-0 cursor-pointer text-white/80">
<CircularText text="EXPLORE THE GALAXY " spinDuration={20} radius={64} />
</div>
)}
</div>
</div>
);
}