"use client";
import * as React from "react";
import { motion } from "framer-motion";
import { cn } from "../lib/utils";
import { transitions } from "../lib/motion";
const SIZES = {
xs: "size-5 text-[9px]",
sm: "size-6 text-[10px]",
md: "size-8 text-xs",
lg: "size-10 text-sm",
} as const;
/** Stable, soft background per name, so the same person keeps one color. */
function hueFor(seed: string): number {
let hash = 0;
for (let index = 0; index < seed.length; index += 1) hash = (hash * 31 + seed.charCodeAt(index)) >>> 0;
return hash % 360;
}
function initials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
return ((parts[0]?.[0] ?? "") + (parts.length > 1 ? (parts.at(-1)?.[0] ?? "") : "")).toUpperCase();
}
export interface AvatarProps {
name: string;
src?: string | null;
size?: keyof typeof SIZES;
className?: string;
}
export function Avatar({ name, src, size = "md", className }: AvatarProps) {
const [failed, setFailed] = React.useState(false);
const hue = hueFor(name);
return (
{src && !failed ? (
// eslint-disable-next-line @next/next/no-img-element -- framework-free package
setFailed(true)} />
) : (
initials(name)
)}
);
}
/** Overlapping avatars that spread apart on hover. */
export function AvatarGroup({
people,
max = 4,
size = "sm",
className,
}: {
people: Array<{ name: string; src?: string | null }>;
max?: number;
size?: keyof typeof SIZES;
className?: string;
}) {
const [hovered, setHovered] = React.useState(false);
const visible = people.slice(0, max);
const overflow = people.length - visible.length;
return (