"use client"; import { motion, useScroll, useSpring } from "motion/react"; import { Children, type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type SlideMeta = { id: string; block: "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H"; title: string; status?: "complete" | "placeholder"; }; type DeckProps = { children: ReactNode; slides: SlideMeta[]; className?: string; }; export function Deck({ children, slides, className }: DeckProps) { const containerRef = useRef(null); const items = useMemo(() => Children.toArray(children), [children]); const [activeIndex, setActiveIndex] = useState(0); const { scrollYProgress } = useScroll(); const progress = useSpring(scrollYProgress, { stiffness: 120, damping: 30, mass: 0.4, }); useEffect(() => { const container = containerRef.current; if (!container) return; const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const index = Number((entry.target as HTMLElement).dataset.index); if (!Number.isNaN(index)) setActiveIndex(index); } } }, { root: null, threshold: 0.55 }, ); const sections = container.querySelectorAll("[data-slide]"); sections.forEach((section) => observer.observe(section)); return () => observer.disconnect(); }, [items.length]); useEffect(() => { function scrollToSlide(index: number) { const clamped = Math.max(0, Math.min(items.length - 1, index)); const target = containerRef.current?.querySelector( `[data-slide][data-index="${clamped}"]`, ); if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); } function onKey(event: KeyboardEvent) { const tag = (event.target as HTMLElement | null)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA") return; switch (event.key) { case "ArrowDown": case "ArrowRight": case "PageDown": case " ": event.preventDefault(); scrollToSlide(activeIndex + 1); break; case "ArrowUp": case "ArrowLeft": case "PageUp": event.preventDefault(); scrollToSlide(activeIndex - 1); break; case "Home": event.preventDefault(); scrollToSlide(0); break; case "End": event.preventDefault(); scrollToSlide(items.length - 1); break; case "f": case "F": if (!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); break; } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [activeIndex, items.length]); const activeMeta = slides[activeIndex]; const isFeatureSlide = (index: number) => { const item = items[index]; if (!item || typeof item !== "object" || !("props" in item)) return false; const props = (item as { props?: { variant?: string } }).props; return props?.variant === "feature"; }; const indicatorInverse = isFeatureSlide(activeIndex); return (
{/* Top scroll-progress bar */} {/* Right-side block indicator dots */} {/* Top-left active-slide eyebrow */} {activeMeta && (
BLOCK {activeMeta.block} · {activeMeta.id.toUpperCase()}
)} {items.map((slide, index) => (
{slide}
))}
); }