{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-status-badge",
  "type": "registry:ui",
  "title": "Agent Status Badge",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Dynamic AI agent status badge featuring an interactive 4x4 dot matrix indicator with distinct micro-animations for reading docs, analyzing, debugging, and success states.",
  "dependencies": [
    "framer-motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/agent-status-badge.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  useReducedMotion,\n  type HTMLMotionProps,\n} from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ */\n/* Types & Constants                                                  */\n/* ------------------------------------------------------------------ */\n\nexport type AgentStatus =\n  | \"reading-docs\"\n  | \"analyzing\"\n  | \"debugging\"\n  | \"success\";\n\nexport type AgentBadgeSize = \"sm\" | \"md\" | \"lg\";\nexport type AgentBadgeVariant = \"default\" | \"outline\" | \"ghost\" | \"glass\";\n\nexport const AGENT_STATUSES: AgentStatus[] = [\n  \"reading-docs\",\n  \"analyzing\",\n  \"debugging\",\n  \"success\",\n];\n\nexport const DEFAULT_STATUS_LABELS: Record<AgentStatus, string> = {\n  \"reading-docs\": \"Reading docs\",\n  \"analyzing\": \"Analyzing\",\n  \"debugging\": \"Debugging\",\n  \"success\": \"Success\",\n};\n\n// Checkmark dot indices for 4x4 grid (row, col):\n// (2,0)=8, (3,1)=13, (2,2)=10, (1,3)=7, (0,3)=3\nconst CHECKMARK_DOTS = [8, 13, 10, 7, 3];\n\nexport interface StatusStyleConfig {\n  label: string;\n  badgeBg: string;\n  badgeBorder: string;\n  badgeText: string;\n  glowColor: string;\n  dotActive: string;\n  dotInactive: string;\n  dotGlow: string;\n}\n\nexport const STATUS_CONFIG: Record<AgentStatus, StatusStyleConfig> = {\n  \"reading-docs\": {\n    label: \"Reading docs\",\n    badgeBg: \"bg-sky-500/10 dark:bg-sky-500/15\",\n    badgeBorder: \"border-sky-500/30 dark:border-sky-500/30\",\n    badgeText: \"text-sky-700 dark:text-sky-300\",\n    glowColor: \"rgba(14, 165, 233, 0.22)\",\n    dotActive: \"bg-sky-500 dark:bg-sky-400\",\n    dotInactive: \"bg-sky-500/25 dark:bg-sky-400/20\",\n    dotGlow: \"shadow-[0_0_6px_rgba(56,189,248,0.8)]\",\n  },\n  \"analyzing\": {\n    label: \"Analyzing\",\n    badgeBg: \"bg-violet-500/10 dark:bg-violet-500/15\",\n    badgeBorder: \"border-violet-500/30 dark:border-violet-500/30\",\n    badgeText: \"text-violet-700 dark:text-violet-300\",\n    glowColor: \"rgba(168, 85, 247, 0.22)\",\n    dotActive: \"bg-violet-500 dark:bg-violet-400\",\n    dotInactive: \"bg-violet-500/25 dark:bg-violet-400/20\",\n    dotGlow: \"shadow-[0_0_6px_rgba(168,85,247,0.8)]\",\n  },\n  \"debugging\": {\n    label: \"Debugging\",\n    badgeBg: \"bg-amber-500/10 dark:bg-amber-500/15\",\n    badgeBorder: \"border-amber-500/30 dark:border-amber-500/30\",\n    badgeText: \"text-amber-700 dark:text-amber-300\",\n    glowColor: \"rgba(245, 158, 11, 0.22)\",\n    dotActive: \"bg-amber-500 dark:bg-amber-400\",\n    dotInactive: \"bg-amber-500/25 dark:bg-amber-400/20\",\n    dotGlow: \"shadow-[0_0_6px_rgba(245,158,11,0.8)]\",\n  },\n  \"success\": {\n    label: \"Success\",\n    badgeBg: \"bg-emerald-500/10 dark:bg-emerald-500/15\",\n    badgeBorder: \"border-emerald-500/30 dark:border-emerald-500/30\",\n    badgeText: \"text-emerald-700 dark:text-emerald-300\",\n    glowColor: \"rgba(16, 185, 129, 0.25)\",\n    dotActive: \"bg-emerald-500 dark:bg-emerald-400\",\n    dotInactive: \"bg-emerald-500/25 dark:bg-emerald-400/20\",\n    dotGlow: \"shadow-[0_0_6px_rgba(52,211,153,0.85)]\",\n  },\n};\n\n/* ------------------------------------------------------------------ */\n/* 4x4 Dot Matrix Subcomponent                                        */\n/* ------------------------------------------------------------------ */\n\nexport interface AgentDotGridProps {\n  status: AgentStatus;\n  size?: AgentBadgeSize;\n  className?: string;\n}\n\nexport function AgentDotGrid({\n  status,\n  size = \"md\",\n  className,\n}: AgentDotGridProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const config = STATUS_CONFIG[status];\n\n  // Sizing definitions for the 4x4 matrix\n  const dotSizeClasses: Record<AgentBadgeSize, { dot: string; gap: string }> = {\n    sm: { dot: \"w-[2.5px] h-[2.5px]\", gap: \"gap-[1.5px]\" },\n    md: { dot: \"w-[3px] h-[3px]\", gap: \"gap-[2px]\" },\n    lg: { dot: \"w-[3.5px] h-[3.5px]\", gap: \"gap-[2.5px]\" },\n  };\n\n  const { dot: dotCls, gap: gapCls } = dotSizeClasses[size];\n\n  // Helper calculating per-dot animation variants based on status\n  const getDotAnimation = (index: number) => {\n    if (shouldReduceMotion) {\n      if (status === \"success\") {\n        return {\n          opacity: CHECKMARK_DOTS.includes(index) ? 1 : 0.2,\n          scale: CHECKMARK_DOTS.includes(index) ? 1.1 : 0.8,\n        };\n      }\n      return { opacity: 0.85, scale: 1 };\n    }\n\n    const row = Math.floor(index / 4);\n    const col = index % 4;\n\n    switch (status) {\n      case \"reading-docs\": {\n        // Line-by-line raster scan across each row\n        const delay = row * 0.2 + col * 0.055;\n        return {\n          animate: {\n            opacity: [0.2, 1, 0.45, 0.2],\n            scale: [0.85, 1.3, 0.95, 0.85],\n          },\n          transition: {\n            duration: 1.25,\n            repeat: Infinity,\n            delay,\n            ease: \"easeInOut\",\n          },\n        };\n      }\n\n      case \"analyzing\": {\n        // Concentric radial waves rippling outward from the 4 central dots\n        const dist = Math.hypot(col - 1.5, row - 1.5);\n        const normalized = (dist - 0.707) / (2.121 - 0.707); // 0 (center) to 1 (corners)\n        const delay = normalized * 0.48;\n        return {\n          animate: {\n            opacity: [0.2, 1, 0.2],\n            scale: [0.75, 1.35, 0.75],\n          },\n          transition: {\n            duration: 1.15,\n            repeat: Infinity,\n            delay,\n            ease: \"easeInOut\",\n          },\n        };\n      }\n\n      case \"debugging\": {\n        // Diagnostic radar search probe stepping across coordinates\n        const pingOrder = [\n          0, 5, 10, 15, 12, 9, 6, 3, 1, 4, 11, 14, 2, 7, 8, 13,\n        ];\n        const step = pingOrder.indexOf(index);\n        const delay = step * 0.055;\n        return {\n          animate: {\n            opacity: [0.15, 1, 0.35, 0.9, 0.15],\n            scale: [0.8, 1.35, 0.9, 1.2, 0.8],\n          },\n          transition: {\n            duration: 0.9,\n            repeat: Infinity,\n            delay,\n            ease: \"linear\",\n          },\n        };\n      }\n\n      case \"success\": {\n        // Triumphant checkmark drawing sequence that settles with glowing green\n        const isCheck = CHECKMARK_DOTS.includes(index);\n        if (isCheck) {\n          const checkIndex = CHECKMARK_DOTS.indexOf(index);\n          return {\n            animate: {\n              opacity: [0.3, 1, 0.95, 1],\n              scale: [0.6, 1.4, 1.15, 1.1],\n            },\n            transition: {\n              duration: 2.4,\n              repeat: Infinity,\n              delay: checkIndex * 0.08,\n              ease: \"easeInOut\",\n            },\n          };\n        }\n        return {\n          animate: { opacity: 0.12, scale: 0.75 },\n          transition: { duration: 0.3 },\n        };\n      }\n    }\n  };\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\"grid grid-cols-4 select-none items-center justify-center\", gapCls, className)}\n    >\n      {Array.from({ length: 16 }).map((_, i) => {\n        const anim = getDotAnimation(i);\n        const isCheck = status === \"success\" && CHECKMARK_DOTS.includes(i);\n\n        return (\n          <motion.span\n            key={`${status}-dot-${i}`}\n            initial={false}\n            animate={anim.animate}\n            transition={anim.transition}\n            className={cn(\n              \"rounded-full transition-colors duration-300\",\n              dotCls,\n              isCheck || status !== \"success\" ? config.dotActive : config.dotInactive,\n              isCheck && config.dotGlow\n            )}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Main Component: AgentStatusBadge                                   */\n/* ------------------------------------------------------------------ */\n\nexport interface AgentStatusBadgeProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\" | \"onChange\"> {\n  /** Current AI agent status */\n  status?: AgentStatus;\n  /** Custom label text override (defaults to status-specific label) */\n  label?: React.ReactNode;\n  /** Optional children for custom badge content */\n  children?: React.ReactNode;\n  /** Size variant */\n  size?: AgentBadgeSize;\n  /** Visual variant */\n  variant?: AgentBadgeVariant;\n  /** Whether to render ambient status glow behind badge */\n  glow?: boolean;\n  /** Whether to render the 4x4 animated dot matrix */\n  showGrid?: boolean;\n  /** If true, clicking cycles through the statuses */\n  interactive?: boolean;\n  /** Callback triggered when status is updated */\n  onStatusChange?: (nextStatus: AgentStatus) => void;\n  className?: string;\n}\n\nexport function AgentStatusBadge({\n  status: controlledStatus,\n  label,\n  children,\n  size = \"md\",\n  variant = \"default\",\n  glow = true,\n  showGrid = true,\n  interactive = false,\n  onStatusChange,\n  className,\n  ...props\n}: AgentStatusBadgeProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalStatus, setInternalStatus] =\n    useState<AgentStatus>(\"reading-docs\");\n\n  const currentStatus = controlledStatus ?? internalStatus;\n  const config = STATUS_CONFIG[currentStatus];\n\n  // Cycling interaction\n  const handleCycle = useCallback(() => {\n    if (!interactive) return;\n    const currentIndex = AGENT_STATUSES.indexOf(currentStatus);\n    const nextStatus =\n      AGENT_STATUSES[(currentIndex + 1) % AGENT_STATUSES.length];\n\n    if (controlledStatus === undefined) {\n      setInternalStatus(nextStatus);\n    }\n    onStatusChange?.(nextStatus);\n  }, [interactive, currentStatus, controlledStatus, onStatusChange]);\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (interactive && (e.key === \"Enter\" || e.key === \" \")) {\n      e.preventDefault();\n      handleCycle();\n    }\n  };\n\n  // Dimensions & paddings\n  const sizeClasses: Record<AgentBadgeSize, string> = {\n    sm: \"h-7 px-2.5 gap-2 text-xs\",\n    md: \"h-8 px-3 gap-2.5 text-xs font-medium\",\n    lg: \"h-9 px-3.5 gap-3 text-sm font-medium\",\n  };\n\n  // Visual variants\n  const variantClasses: Record<AgentBadgeVariant, string> = {\n    default: cn(config.badgeBg, config.badgeBorder, config.badgeText),\n    outline: cn(\"bg-transparent\", config.badgeBorder, config.badgeText),\n    ghost: cn(\"bg-transparent border-transparent\", config.badgeText),\n    glass: cn(\n      \"backdrop-blur-md bg-white/40 dark:bg-neutral-900/40\",\n      config.badgeBorder,\n      config.badgeText\n    ),\n  };\n\n  const displayText = label ?? children ?? config.label;\n\n  return (\n    <motion.div\n      role={interactive ? \"button\" : \"status\"}\n      tabIndex={interactive ? 0 : undefined}\n      aria-label={`AI agent status: ${typeof displayText === \"string\" ? displayText : currentStatus}`}\n      aria-live=\"polite\"\n      onClick={handleCycle}\n      onKeyDown={handleKeyDown}\n      whileHover={interactive && !shouldReduceMotion ? { scale: 1.03 } : undefined}\n      whileTap={interactive && !shouldReduceMotion ? { scale: 0.97 } : undefined}\n      style={{\n        boxShadow:\n          glow && variant !== \"ghost\"\n            ? `0 0 16px ${config.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.1)`\n            : undefined,\n      }}\n      className={cn(\n        \"relative inline-flex items-center rounded-full border transition-all duration-300 select-none\",\n        interactive && \"cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-neutral-400\",\n        sizeClasses[size],\n        variantClasses[variant],\n        className\n      )}\n      {...props}\n    >\n      {/* 4x4 animated matrix */}\n      {showGrid && <AgentDotGrid status={currentStatus} size={size} />}\n\n      {/* Animated label text */}\n      <div className=\"relative overflow-hidden\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.span\n            key={String(displayText)}\n            initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 5 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: shouldReduceMotion ? 0 : -5 }}\n            transition={{ duration: 0.16, ease: \"easeOut\" }}\n            className=\"inline-block whitespace-nowrap tracking-tight font-medium\"\n          >\n            {displayText}\n          </motion.span>\n        </AnimatePresence>\n      </div>\n\n      {/* Interactive indicator pill */}\n      {interactive && (\n        <span\n          aria-hidden=\"true\"\n          className=\"ml-0.5 text-[10px] opacity-40 hover:opacity-80 transition-opacity\"\n        >\n          ⇄\n        </span>\n      )}\n    </motion.div>\n  );\n}\n\nexport default AgentStatusBadge;\n",
      "type": "registry:ui",
      "target": "components/ui/agent-status-badge.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
