"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`}
)}
);
}