"use client"; import { CheckIcon, LockIcon, PlusIcon, TrophyIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type FormEvent, useEffect, useId, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export interface PollOption { id: string; label: string; votes: number; } export interface Poll { question: string; options: PollOption[]; author?: string; closed?: boolean; } export interface LivePollProps { defaultPoll?: Poll; defaultVote?: string | null; simulateCrowd?: boolean; you?: string; onVote?: (optionId: string) => void; onClose?: (poll: Poll) => void; className?: string; } const MAX_OPTIONS = 5; const ease = [0.23, 1, 0.32, 1] as const; let uid = 0; const nextId = () => `o${++uid}`; function Rolling({ value, suffix = "", reduce }: { value: number; suffix?: string; reduce: boolean | null }) { return ( {value} {suffix} ); } export function LivePoll({ defaultPoll, defaultVote = null, simulateCrowd = false, you = "You", onVote, onClose, className, }: LivePollProps) { const reduce = useReducedMotionAfterMount(); const [poll, setPoll] = useState(defaultPoll ?? null); const [vote, setVote] = useState(defaultVote); const [pulse, setPulse] = useState(null); const previous = useRef<{ poll: Poll; vote: string | null } | null>(null); useEffect(() => { if (!simulateCrowd || !poll || poll.closed) return; let timer: ReturnType; const tick = () => { setPoll((p) => { if (!p || p.closed) return p; const weights = p.options.map((o) => o.votes + 2); let r = Math.random() * weights.reduce((a, b) => a + b, 0); const idx = weights.findIndex((w) => (r -= w) < 0); const target = p.options[Math.max(0, idx)]; setPulse(target.id); return { ...p, options: p.options.map((o) => (o.id === target.id ? { ...o, votes: o.votes + 1 } : o)) }; }); timer = setTimeout(tick, 1400 + Math.random() * 1800); }; timer = setTimeout(tick, 1600); return () => clearTimeout(timer); }, [simulateCrowd, poll?.closed, poll === null]); const castVote = (id: string) => { if (!poll || poll.closed || id === vote) return; setPoll({ ...poll, options: poll.options.map((o) => o.id === id ? { ...o, votes: o.votes + 1 } : o.id === vote ? { ...o, votes: Math.max(0, o.votes - 1) } : o, ), }); setVote(id); setPulse(null); onVote?.(id); }; const close = () => { if (!poll) return; const done = { ...poll, closed: true }; setPoll(done); onClose?.(done); }; return (
{poll ? ( { previous.current = { poll, vote }; setPoll(null); setVote(null); }} onVote={castVote} poll={poll} pulse={pulse} reduce={reduce} vote={vote} you={you} /> ) : ( { const prev = previous.current; if (!prev) return; setPoll(prev.poll); setVote(prev.vote); } : undefined } onPost={(question, labels) => { setVote(null); setPoll({ question, author: you, options: labels.map((label) => ({ id: nextId(), label, votes: 0 })) }); }} reduce={reduce} /> )}
); } function PollView({ poll, vote, pulse, you, reduce, onVote, onClose, onNew, }: { poll: Poll; vote: string | null; pulse: string | null; you: string; reduce: boolean | null; onVote: (id: string) => void; onClose: () => void; onNew: () => void; }) { const total = poll.options.reduce((n, o) => n + o.votes, 0); const showResults = vote !== null || !!poll.closed; const top = Math.max(...poll.options.map((o) => o.votes)); const mine = poll.author === you; const groupId = useId(); return (
{poll.closed ? ( Final results ) : ( {!reduce && } Live )} {poll.author ? `${poll.author} asked` : "Poll"} {total === 1 ? "vote" : "votes"}

{poll.question}

{poll.options.map((o) => { const pct = total ? Math.round((o.votes / total) * 100) : 0; const chosen = vote === o.id; const winner = poll.closed && o.votes === top && top > 0; return ( ); })}
{poll.closed ? "Voting has ended" : vote ? "You can change your vote until it closes" : "Vote to see the results"} {poll.closed ? ( ) : ( mine && ( ) )}
); } function Composer({ onPost, onCancel, reduce, }: { onPost: (question: string, options: string[]) => void; onCancel?: () => void; reduce: boolean | null; }) { const qId = useId(); const [question, setQuestion] = useState(""); const [options, setOptions] = useState(() => [ { id: nextId(), label: "" }, { id: nextId(), label: "" }, ]); const refs = useRef(new Map()); const focusNext = useRef(null); useEffect(() => { if (!focusNext.current) return; refs.current.get(focusNext.current)?.focus(); focusNext.current = null; }); const filled = options.map((o) => o.label.trim()).filter(Boolean); const ready = question.trim().length > 0 && filled.length >= 2; const add = () => { if (options.length >= MAX_OPTIONS) return; const id = nextId(); focusNext.current = id; setOptions((os) => [...os, { id, label: "" }]); }; const submit = (e: FormEvent) => { e.preventDefault(); if (ready) onPost(question.trim(), filled); }; return (

New poll

{options.length}/{MAX_OPTIONS} options
setQuestion(e.target.value)} placeholder="Ask a question…" value={question} />
    {options.map((o, i) => (
    {i + 1} setOptions((os) => os.map((x) => (x.id === o.id ? { ...x, label: e.target.value } : x)))} onKeyDown={(e) => { if (e.key === "Enter" && i === options.length - 1 && o.label.trim()) { e.preventDefault(); add(); } }} placeholder={i === 0 ? "First choice" : i === 1 ? "Second choice" : "Another choice"} ref={(el) => { if (el) refs.current.set(o.id, el); else refs.current.delete(o.id); }} value={o.label} />
    ))}
{onCancel && ( )}
); }