"use client"; import { AlertTriangle, Check, Circle, Info, LoaderCircle, X, type LucideIcon, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion, type HTMLMotionProps, type Variants, } from "motion/react"; import { type ReactNode } from "react"; import { cn } from "@/lib/utils"; export type AnimatedBadgeStatus = | "neutral" | "info" | "success" | "warning" | "danger" | "loading"; export type AnimatedBadgeSize = "sm" | "md"; export interface AnimatedBadgeProps extends Omit< HTMLMotionProps<"span">, "children" > { status?: AnimatedBadgeStatus; size?: AnimatedBadgeSize; children?: ReactNode; icon?: ReactNode; showIcon?: boolean; pulse?: boolean; contentKey?: string | number; } const STATUS_CLASS: Record = { neutral: "border-(--color-border) bg-(--color-bg-elev) text-(--color-fg-muted)", info: "border-(--color-accent)/30 bg-(--color-accent)/10 text-(--color-accent)", success: "border-(--color-success)/30 bg-(--color-success)/10 text-(--color-success)", warning: "border-(--color-warning)/35 bg-(--color-warning)/10 text-(--color-warning)", danger: "border-(--color-danger)/30 bg-(--color-danger)/10 text-(--color-danger)", loading: "border-(--color-violet)/30 bg-(--color-violet)/10 text-(--color-violet)", }; const SIZE_CLASS: Record = { sm: "h-6 gap-1.5 px-2 text-[11px]", md: "h-8 gap-2 px-3 text-xs", }; const ICON_CLASS: Record = { sm: "h-3 w-3", md: "h-3.5 w-3.5", }; const ICONS: Record = { neutral: Circle, info: Info, success: Check, warning: AlertTriangle, danger: X, loading: LoaderCircle, }; const MORPH_EASE = [0.16, 1, 0.3, 1] as const; const ICON_ROLL_VARIANTS: Variants = { initial: { opacity: 0.72, y: "80%", scale: 0.92, rotate: -8, filter: "blur(6px)", }, animate: { opacity: 1, y: "0%", scale: 1, rotate: 0, filter: "blur(0px)", transition: { y: { type: "spring", stiffness: 210, damping: 24, mass: 0.85 }, scale: { type: "spring", stiffness: 250, damping: 24, mass: 0.75 }, rotate: { duration: 0.28, ease: MORPH_EASE }, opacity: { duration: 0.28, ease: MORPH_EASE }, filter: { duration: 0.42, ease: MORPH_EASE }, }, }, exit: { opacity: 0.5, y: "-80%", scale: 0.96, rotate: 8, filter: "blur(6px)", transition: { duration: 0.22, ease: MORPH_EASE }, }, }; const TEXT_ROLL_VARIANTS: Variants = { initial: { opacity: 0.76, y: "85%", filter: "blur(6px)" }, animate: { opacity: 1, y: "0%", filter: "blur(0px)", transition: { y: { type: "spring", stiffness: 210, damping: 24, mass: 0.85 }, opacity: { duration: 0.3, ease: MORPH_EASE }, filter: { duration: 0.42, ease: MORPH_EASE }, }, }, exit: { opacity: 0.5, y: "-85%", filter: "blur(6px)", transition: { duration: 0.2, ease: MORPH_EASE }, }, }; export function AnimatedBadge({ status = "neutral", size = "md", children, icon, showIcon = true, pulse = status === "loading", contentKey, className, ...rest }: AnimatedBadgeProps) { const reduce = useReducedMotion(); const Icon = ICONS[status]; const resolvedContentKey = contentKey ?? (typeof children === "string" || typeof children === "number" ? children : status); return ( {pulse && !reduce ? ( ) : null} {showIcon ? ( {status === "loading" && !reduce && !icon ? ( ) : ( (icon ?? ) )} ) : null} {children != null ? ( {children} ) : null} ); }