"use client"; import { useEffect, useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Plus, RotateCcw, Sparkles, X } from "lucide-react"; import { gsap } from "@/lib/gsap"; import { wheelEase } from "@/lib/motion"; import { celebrateConfetti } from "@/lib/confetti"; import { useSound } from "@/components/providers/sound-provider"; import { useLocalStorage } from "@/hooks/use-local-storage"; import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion"; const DEFAULT_OPTIONS = ["1", "2", "3", "4", "5", "6"]; /** Natürliche, gedeckte Farben – weiße Schrift bleibt lesbar. */ const PALETTE = [ "#33523f", "#a8623f", "#b98a3c", "#5e7a5e", "#6b7f8c", "#8a6b52", "#5f8a80", "#9a5b45", "#4f6b8a", "#8c7a4f", "#5c6e4f", "#3f5e66", ]; const C = 210; const R = 196; const MIN_OPTIONS = 2; const MAX_OPTIONS = 12; /** Winkel → Punkt auf dem Kreis (0° = oben, im Uhrzeigersinn). */ function polar(angleDeg: number, radius: number): [number, number] { const a = (angleDeg * Math.PI) / 180; return [C + radius * Math.sin(a), C - radius * Math.cos(a)]; } export default function WheelOfFortune() { const [options, setOptions] = useLocalStorage( "wheel-options", DEFAULT_OPTIONS, ); const [spinning, setSpinning] = useState(false); const [winner, setWinner] = useState(null); const wheelRef = useRef(null); const pointerRef = useRef(null); const rotationRef = useRef(0); const lastTickRef = useRef(-1); const optionInputRefs = useRef>([]); const justAddedRef = useRef(false); const { play } = useSound(); const reduced = usePrefersReducedMotion(); const labels = options.map((o, i) => o.trim() || `Option ${i + 1}`); const n = labels.length; const seg = 360 / n; const spin = () => { if (spinning || n < MIN_OPTIONS) return; const winnerIndex = Math.floor(Math.random() * n); setWinner(null); setSpinning(true); play("whoosh"); lastTickRef.current = -1; // Zufällige Position innerhalb des Gewinner-Segments const offset = 0.18 + Math.random() * 0.64; const targetAngle = winnerIndex * seg + seg * offset; const current = rotationRef.current; const currentMod = ((current % 360) + 360) % 360; const delta = (((360 - targetAngle - currentMod) % 360) + 360) % 360; const target = current + 360 * 6 + delta; const dur = reduced ? 0.5 : 5.6; gsap.to(wheelRef.current, { rotation: target, svgOrigin: `${C} ${C}`, duration: dur, ease: reduced ? "power1.out" : wheelEase, onUpdate: () => { if (reduced) return; const rot = Number(gsap.getProperty(wheelRef.current, "rotation")); const m = ((rot % 360) + 360) % 360; const idx = Math.floor(((360 - m) % 360) / seg); if (idx !== lastTickRef.current) { lastTickRef.current = idx; play("tick"); gsap.fromTo( pointerRef.current, { rotation: -16 }, { rotation: 0, duration: 0.22, ease: "elastic.out(2.2, 0.4)", transformOrigin: "50% 12%", }, ); } }, onComplete: () => { rotationRef.current = target; setSpinning(false); setWinner(winnerIndex); play("win"); celebrateConfetti(); }, }); }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const activeTag = document.activeElement?.tagName.toLowerCase(); if (activeTag === "input" || activeTag === "textarea") return; if (e.code === "Space") { e.preventDefault(); spin(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [spinning, n]); const updateOption = (i: number, value: string) => { if (spinning) return; setOptions((opts) => opts.map((o, idx) => (idx === i ? value : o))); setWinner(null); }; const addOption = () => { if (spinning || n >= MAX_OPTIONS) return; setOptions((opts) => [...opts, ""]); setWinner(null); justAddedRef.current = true; }; useEffect(() => { if (!justAddedRef.current) return; justAddedRef.current = false; const last = optionInputRefs.current[options.length - 1]; last?.scrollIntoView({ block: "nearest", behavior: "smooth" }); last?.focus(); }, [options.length]); const removeOption = (i: number) => { if (spinning || n <= MIN_OPTIONS) return; setOptions((opts) => opts.filter((_, idx) => idx !== i)); setWinner(null); }; const resetOptions = () => { if (spinning) return; setOptions(DEFAULT_OPTIONS); setWinner(null); }; const fontSize = n <= 6 ? 15 : n <= 9 ? 13 : 11.5; return (
{/* Rad */}
{/* Zeiger */}
{/* äußerer Ring */} {labels.map((label, i) => { const [x0, y0] = polar(i * seg, R); const [x1, y1] = polar((i + 1) * seg, R); const large = seg > 180 ? 1 : 0; const isWinner = winner === i; const dimmed = winner !== null && !isWinner; return ( {label.length > 16 ? `${label.slice(0, 15)}…` : label} ); })} {/* Nabe */}
{winner !== null && ( {labels[winner]}! )}
{spinning ? "Dreht sich …" : "Drehen!"}
{/* Optionen-Editor */}

Optionen ({n}/{MAX_OPTIONS})

{options.map((option, i) => (
{ optionInputRefs.current[i] = el; }} value={option} onChange={(e) => updateOption(i, e.target.value)} disabled={spinning} maxLength={24} placeholder={`Option ${i + 1}`} aria-label={`Option ${i + 1}`} className="w-full rounded-xl border border-line bg-paper/50 px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-[#0d7a57] focus:bg-surface focus:ring-2 focus:ring-[#0d7a57]/20 disabled:cursor-not-allowed disabled:opacity-60" />
))}

{MIN_OPTIONS}–{MAX_OPTIONS} Optionen. Deine Liste bleibt automatisch gespeichert.

); }