import { useState, useEffect, useRef } from "react"; // ─── Scale wrapper (same pattern as other template previews) const INTERNAL_W = 480; const INTERNAL_H = 270; function ScaledCanvas({ children }: { children: React.ReactNode }) { const ref = useRef(null); const [scale, setScale] = useState(0.5); useEffect(() => { const el = ref.current; if (!el) return; const update = () => { const s = Math.max(el.offsetWidth / INTERNAL_W, el.offsetHeight / INTERNAL_H); if (s > 0) setScale(s); }; update(); const obs = new ResizeObserver(update); obs.observe(el); return () => obs.disconnect(); }, []); return (
{children}
); } // ─── Design tokens const ACCENT = "#00FF41"; const BLACK = "#000000"; const MUTED = "#00FF4166"; const FONT = "'Fira Code', 'Courier New', monospace"; // ─── Digital rain background (lightweight CSS version for preview) function RainBackground() { return (
{Array.from({ length: 20 }, (_, i) => (
{"アイウ01\nカキク23\nサシス45\nタチツ67\nナニヌ89\nハヒフAB\nマミムCD".split("\n").join("\n")}
))}
); } function useSpring( active: boolean, options: { stiffness?: number; damping?: number; delay?: number } = {} ) { const { stiffness = 200, damping = 18, delay = 0 } = options; const [value, setValue] = useState(0); useEffect(() => { if (!active) { setValue(0); return; } let start: number | null = null; let raf: number; let vel = 0; let pos = 0; function tick(t: number) { if (!start) start = t + delay; if (t < start) { raf = requestAnimationFrame(tick); return; } const force = -stiffness * (pos - 1) - damping * vel; vel += force * (1 / 60); pos += vel * (1 / 60); setValue(pos); if (Math.abs(pos - 1) > 0.001 || Math.abs(vel) > 0.001) { raf = requestAnimationFrame(tick); } else { setValue(1); } } raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [active, delay, stiffness, damping]); return value; } function useCountUp(active: boolean, target: number, duration = 1200) { const [val, setVal] = useState(0); useEffect(() => { if (!active) { setVal(0); return; } let start: number | null = null; let raf: number; function tick(t: number) { if (!start) start = t; const p = Math.min((t - start) / duration, 1); const ease = 1 - Math.pow(1 - p, 3); setVal(Math.round(ease * target)); if (p < 1) raf = requestAnimationFrame(tick); else setVal(target); } raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [active, target, duration]); return val; } // ─── Glitch text decode hook function useGlitchText(active: boolean, text: string, delay = 0) { const GLITCH = "アイウ0123!@#$%"; const [display, setDisplay] = useState(""); useEffect(() => { if (!active) { setDisplay(""); return; } let raf: number; let frameCount = 0; const chars = text.split(""); const framesPerChar = 3; function tick() { frameCount++; const revealed = Math.floor((frameCount - delay / 16) / framesPerChar); const result = chars.map((ch, i) => { if (ch === " ") return " "; if (i < revealed) return ch; if (i < revealed + 4 && frameCount > delay / 16) { return GLITCH[Math.floor(Math.random() * GLITCH.length)]; } return " "; }).join(""); setDisplay(result); if (revealed < chars.length + 4) { raf = requestAnimationFrame(tick); } else { setDisplay(text); } } raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [active, text, delay]); return display; } // ─── Slide 1: Matrix Title (character decode) function SlideMatrixTitle({ active }: { active: boolean }) { const decoded = useGlitchText(active, "SYSTEM ONLINE", 200); const subS = useSpring(active, { stiffness: 160, damping: 20, delay: 800 }); return (
{decoded || "\u00A0"}
Digital rain on a black void
); } // ─── Slide 2: Data Stream (terminal list) const LIST_ITEMS = ["Lightning fast performance", "Zero config setup", "Built-in analytics"]; function SlideDataStream({ active }: { active: boolean }) { const [current, setCurrent] = useState(-1); useEffect(() => { if (!active) { setCurrent(-1); return; } let i = 0; const timers: ReturnType[] = []; const go = () => { setCurrent(i); i++; if (i < LIST_ITEMS.length) timers.push(setTimeout(go, 500)); }; timers.push(setTimeout(go, 200)); return () => timers.forEach(clearTimeout); }, [active]); return (
{LIST_ITEMS.map((item, i) => { const shown = i <= current; const dimmed = shown && i < current; return (
{">"} {String(i + 1).padStart(2, "0")} {item}
); })}
); } // ─── Slide 3: Cipher Metric (number decode) function SlideCipherMetric({ active }: { active: boolean }) { const num = useCountUp(active, 97, 1200); const cardS = useSpring(active, { stiffness: 160, damping: 20, delay: 900 }); const glowPulse = active ? 0.7 + Math.sin(Date.now() * 0.003) * 0.3 : 0; return (
{active ? num : 0} %
System Uptime
Based on 12,000+ requests
); } // ─── Slide indicator dots function SlideDots({ total, current, onDotClick }: { total: number; current: number; onDotClick: (i: number) => void }) { return (
{Array.from({ length: total }, (_, i) => (
); } // ─── Main const SLIDES = [SlideMatrixTitle, SlideDataStream, SlideCipherMetric]; const SLIDE_DURATION = 3500; export default function MatrixPreview({ thumbnailMode = false }: { thumbnailMode?: boolean } = {}) { const [current, setCurrent] = useState(0); const [active, setActive] = useState(false); useEffect(() => { // Side cards play the first slide's intro once and rest on its settled // state (no slide cycling). Pinning to slide 0 also means the animation // restarts from the top when the card returns to center. setCurrent(0); if (thumbnailMode) { setActive(true); return; } setActive(false); const t = setTimeout(() => setActive(true), 200); return () => clearTimeout(t); }, [thumbnailMode]); useEffect(() => { if (thumbnailMode) return; const id = setInterval(() => { setActive(false); setTimeout(() => { setCurrent((c) => (c + 1) % SLIDES.length); setActive(true); }, 150); }, SLIDE_DURATION); return () => clearInterval(id); }, [thumbnailMode]); const handleDot = (i: number) => { setActive(false); setTimeout(() => { setCurrent(i); setActive(true); }, 100); }; const SlideComp = SLIDES[current]; return (
); }