{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-badge","type":"registry:component","title":"Animated Badge","description":"Status badge with animated state icons, pulse feedback and compact size variants.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/animated-badge.tsx","type":"registry:component","target":"@components/motion/animated-badge.tsx","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 { EASE_OUT } from \"@/lib/ease\";\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: \"border-border bg-card text-muted-foreground\",\n  info: \"border-primary/30 bg-primary/10 text-primary\",\n  success: \"border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\",\n  warning: \"border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400\",\n  danger: \"border-destructive/30 bg-destructive/10 text-destructive\",\n  loading: \"border-primary/30 bg-primary/10 text-primary\",\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 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: EASE_OUT },\n      opacity: { duration: 0.28, ease: EASE_OUT },\n      filter: { duration: 0.42, ease: EASE_OUT },\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: EASE_OUT },\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: EASE_OUT },\n      filter: { duration: 0.42, ease: EASE_OUT },\n    },\n  },\n  exit: {\n    opacity: 0.5,\n    y: \"-85%\",\n    filter: \"blur(6px)\",\n    transition: { duration: 0.2, ease: EASE_OUT },\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":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","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"}]}