"use client"; import { SparklesIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export interface Typer { id: string; name: string; initials: string; avatarClassName?: string; } const ease = [0.23, 1, 0.32, 1] as const; function Dots({ size = "md", className }: { size?: "sm" | "md"; className?: string }) { const reduce = useReducedMotionAfterMount(); return ( {[0, 1, 2].map((i) => ( ))} ); } function Avatar({ person, className }: { person: Typer; className?: string }) { return ( {person.initials} ); } export interface TypingDotsProps { label?: string; className?: string; } export function TypingDots({ label = "Someone is typing", className }: TypingDotsProps) { return ( ); } export interface TypingNameProps { person: Typer; className?: string; } export function TypingName({ person, className }: TypingNameProps) { return ( {person.name} is typing ); } function crowdLabel(people: Typer[]) { const names = people.map((p) => p.name.split(" ")[0]); if (names.length === 0) return ""; if (names.length === 1) return `${names[0]} is typing`; if (names.length === 2) return `${names[0]} and ${names[1]} are typing`; return `${names.length} people are typing`; } export interface TypingCrowdProps { people: Typer[]; maxAvatars?: number; className?: string; } export function TypingCrowd({ people, maxAvatars = 3, className }: TypingCrowdProps) { const reduce = useReducedMotionAfterMount(); const shown = people.slice(0, maxAvatars); const label = crowdLabel(people); const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 520, damping: 34 }; return (
{shown.map((p, i) => ( 0 && "-ms-1")} exit={{ opacity: 0, scale: 0.85, transition: { duration: 0.14 } }} initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.85, x: -6 }} key={p.id} layout style={{ zIndex: maxAvatars - i }} transition={spring} > ))} {people.length > 0 && ( )} {label}
); } export interface ComposingIndicatorProps { name?: string; label?: string; showElapsed?: boolean; className?: string; } export function ComposingIndicator({ name = "Lumen", label = "Composing a reply", showElapsed = true, className, }: ComposingIndicatorProps) { const reduce = useReducedMotionAfterMount(); const [seconds, setSeconds] = useState(0); useEffect(() => { if (!showElapsed) return; const id = setInterval(() => setSeconds((s) => s + 1), 1000); return () => clearInterval(id); }, [showElapsed]); return ( {name} is {label.toLowerCase()} {label} {showElapsed && ( {seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`} )} ); }