{"slug":"animated-badge","name":"Animated Badge","description":"Status badge with animated state icons, pulse feedback and compact size variants.","category":"motion","source_url":"https://beui.saura3h.xyz/r/animated-badge/raw","detail_url":"https://beui.saura3h.xyz/r/animated-badge","raw_url":"https://beui.saura3h.xyz/r/animated-badge/raw","page_url":"https://beui.saura3h.xyz/components/motion/animated-badge","dependencies":["lucide-react","motion","react"],"internal":["@/components/motion/animated-badge","@/lib/utils"],"files":[{"path":"components/motion/animated-badge.tsx","type":"component","content":"\"use client\";\n\nimport {\n  AlertTriangle,\n  Check,\n  Circle,\n  Info,\n  LoaderCircle,\n  X,\n  type LucideIcon,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type HTMLMotionProps,\n  type Variants,\n} from \"motion/react\";\nimport { type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AnimatedBadgeStatus =\n  | \"neutral\"\n  | \"info\"\n  | \"success\"\n  | \"warning\"\n  | \"danger\"\n  | \"loading\";\n\nexport type AnimatedBadgeSize = \"sm\" | \"md\";\n\nexport interface AnimatedBadgeProps extends Omit<\n  HTMLMotionProps<\"span\">,\n  \"children\"\n> {\n  status?: AnimatedBadgeStatus;\n  size?: AnimatedBadgeSize;\n  children?: ReactNode;\n  icon?: ReactNode;\n  showIcon?: boolean;\n  pulse?: boolean;\n  contentKey?: string | number;\n}\n\nconst STATUS_CLASS: Record<AnimatedBadgeStatus, string> = {\n  neutral:\n    \"border-(--color-border) bg-(--color-bg-elev) text-(--color-fg-muted)\",\n  info: \"border-(--color-accent)/30 bg-(--color-accent)/10 text-(--color-accent)\",\n  success:\n    \"border-(--color-success)/30 bg-(--color-success)/10 text-(--color-success)\",\n  warning:\n    \"border-(--color-warning)/35 bg-(--color-warning)/10 text-(--color-warning)\",\n  danger:\n    \"border-(--color-danger)/30 bg-(--color-danger)/10 text-(--color-danger)\",\n  loading:\n    \"border-(--color-violet)/30 bg-(--color-violet)/10 text-(--color-violet)\",\n};\n\nconst SIZE_CLASS: Record<AnimatedBadgeSize, string> = {\n  sm: \"h-6 gap-1.5 px-2 text-[11px]\",\n  md: \"h-8 gap-2 px-3 text-xs\",\n};\n\nconst ICON_CLASS: Record<AnimatedBadgeSize, string> = {\n  sm: \"h-3 w-3\",\n  md: \"h-3.5 w-3.5\",\n};\n\nconst ICONS: Record<AnimatedBadgeStatus, LucideIcon> = {\n  neutral: Circle,\n  info: Info,\n  success: Check,\n  warning: AlertTriangle,\n  danger: X,\n  loading: LoaderCircle,\n};\n\nconst MORPH_EASE = [0.16, 1, 0.3, 1] as const;\n\nconst ICON_ROLL_VARIANTS: Variants = {\n  initial: {\n    opacity: 0.72,\n    y: \"80%\",\n    scale: 0.92,\n    rotate: -8,\n    filter: \"blur(6px)\",\n  },\n  animate: {\n    opacity: 1,\n    y: \"0%\",\n    scale: 1,\n    rotate: 0,\n    filter: \"blur(0px)\",\n    transition: {\n      y: { type: \"spring\", stiffness: 210, damping: 24, mass: 0.85 },\n      scale: { type: \"spring\", stiffness: 250, damping: 24, mass: 0.75 },\n      rotate: { duration: 0.28, ease: MORPH_EASE },\n      opacity: { duration: 0.28, ease: MORPH_EASE },\n      filter: { duration: 0.42, ease: MORPH_EASE },\n    },\n  },\n  exit: {\n    opacity: 0.5,\n    y: \"-80%\",\n    scale: 0.96,\n    rotate: 8,\n    filter: \"blur(6px)\",\n    transition: { duration: 0.22, ease: MORPH_EASE },\n  },\n};\n\nconst TEXT_ROLL_VARIANTS: Variants = {\n  initial: { opacity: 0.76, y: \"85%\", filter: \"blur(6px)\" },\n  animate: {\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: {\n      y: { type: \"spring\", stiffness: 210, damping: 24, mass: 0.85 },\n      opacity: { duration: 0.3, ease: MORPH_EASE },\n      filter: { duration: 0.42, ease: MORPH_EASE },\n    },\n  },\n  exit: {\n    opacity: 0.5,\n    y: \"-85%\",\n    filter: \"blur(6px)\",\n    transition: { duration: 0.2, ease: MORPH_EASE },\n  },\n};\n\nexport function AnimatedBadge({\n  status = \"neutral\",\n  size = \"md\",\n  children,\n  icon,\n  showIcon = true,\n  pulse = status === \"loading\",\n  contentKey,\n  className,\n  ...rest\n}: AnimatedBadgeProps) {\n  const reduce = useReducedMotion();\n  const Icon = ICONS[status];\n  const resolvedContentKey =\n    contentKey ??\n    (typeof children === \"string\" || typeof children === \"number\"\n      ? children\n      : status);\n\n  return (\n    <motion.span\n      layout\n      transition={{ type: \"spring\", stiffness: 420, damping: 30, mass: 0.7 }}\n      className={cn(\n        \"relative inline-flex shrink-0 items-center overflow-hidden whitespace-nowrap rounded-full border font-medium tabular-nums\",\n        \"transition-colors duration-300\",\n        STATUS_CLASS[status],\n        SIZE_CLASS[size],\n        className,\n      )}\n      {...rest}\n    >\n      {pulse && !reduce ? (\n        <motion.span\n          aria-hidden\n          className=\"absolute inset-0 rounded-full bg-current opacity-10\"\n          animate={{ scale: [0.94, 1.08, 0.94], opacity: [0.08, 0.16, 0.08] }}\n          transition={{ duration: 1.6, repeat: Infinity, ease: \"easeInOut\" }}\n        />\n      ) : null}\n      {showIcon ? (\n        <span className=\"relative z-10 inline-flex items-center justify-center overflow-hidden\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span\n              key={status}\n              aria-hidden\n              data-badge-icon\n              variants={ICON_ROLL_VARIANTS}\n              initial={reduce ? false : \"initial\"}\n              animate={reduce ? { opacity: 1 } : \"animate\"}\n              exit={reduce ? undefined : \"exit\"}\n              className=\"inline-flex will-change-transform\"\n            >\n              {status === \"loading\" && !reduce && !icon ? (\n                <motion.span\n                  animate={{ rotate: 360 }}\n                  transition={{ duration: 1, repeat: Infinity, ease: \"linear\" }}\n                  className=\"inline-flex\"\n                >\n                  <Icon className={ICON_CLASS[size]} />\n                </motion.span>\n              ) : (\n                (icon ?? <Icon className={ICON_CLASS[size]} />)\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ) : null}\n      {children != null ? (\n        <span className=\"relative z-10 inline-flex overflow-hidden\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span\n              key={resolvedContentKey}\n              data-badge-label\n              variants={TEXT_ROLL_VARIANTS}\n              initial={reduce ? false : \"initial\"}\n              animate={reduce ? { opacity: 1 } : \"animate\"}\n              exit={reduce ? undefined : \"exit\"}\n              className=\"inline-block will-change-transform\"\n            >\n              {children}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ) : null}\n    </motion.span>\n  );\n}\n"},{"path":"components/previews/motion/animated-badge.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { AnimatedBadge, type AnimatedBadgeStatus } from \"@/components/motion/animated-badge\";\n\nconst STATES: Array<{ status: AnimatedBadgeStatus; label: string }> = [\n  { status: \"loading\", label: \"Syncing\" },\n  { status: \"success\", label: \"Synced\" },\n  { status: \"warning\", label: \"Review\" },\n  { status: \"danger\", label: \"Failed\" },\n];\n\nexport function AnimatedBadgePreview() {\n  const [active, setActive] = useState(0);\n  const state = STATES[active];\n\n  useEffect(() => {\n    const id = window.setInterval(() => {\n      setActive((current) => (current + 1) % STATES.length);\n    }, 1600);\n\n    return () => window.clearInterval(id);\n  }, []);\n\n  return (\n    <div className=\"flex flex-col items-center gap-6\">\n      <div className=\"flex h-16 items-center justify-center\">\n        <AnimatedBadge status={state.status} size=\"md\" aria-live=\"polite\">\n          {state.label}\n        </AnimatedBadge>\n      </div>\n\n      <div className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n        <AnimatedBadge status=\"neutral\" size=\"sm\">Queued</AnimatedBadge>\n        <AnimatedBadge status=\"info\" size=\"sm\">Live</AnimatedBadge>\n        <AnimatedBadge status=\"loading\" size=\"sm\">Indexing</AnimatedBadge>\n        <AnimatedBadge status=\"success\" size=\"sm\">Verified</AnimatedBadge>\n        <AnimatedBadge status=\"warning\" size=\"sm\">Pending</AnimatedBadge>\n        <AnimatedBadge status=\"danger\" size=\"sm\">Blocked</AnimatedBadge>\n      </div>\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}