{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "halo-badge",
  "type": "registry:ui",
  "description": "Badge with optional layout animation, live status dot, and gradient rim aligned with Halo Button / Card",
  "dependencies": [
    "class-variance-authority",
    "motion"
  ],
  "registryDependencies": [
    "badge"
  ],
  "files": [
    {
      "path": "registry/default/ui/halo-badge.tsx",
      "content": "\"use client\"\n\n/**\n * Badge with optional layout animation on content changes (`tabular-nums` for counts),\n * a “live” status dot (opacity-only pulse), and the same frosted fill + animated\n * gradient rim + layered shadow language as `HaloButton` / `HaloCard`.\n */\nimport { useMemo, useState, type ComponentProps, type ReactNode } from \"react\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { badgeVariants } from \"@/components/ui/badge\"\n\n/* ─── Rim + fill (self-contained; no halo-card import) ─── */\n\ntype RimVariant = \"default\" | \"success\" | \"destructive\"\n\nconst RIM_BLOB_GRADIENTS: Record<\n  RimVariant,\n  { primaryBg: string; secondaryBg: string; tertiaryBg: string }\n> = {\n  default: {\n    primaryBg: \"linear-gradient(90deg, #ff0080, #7928ca, #00d4ff, #0070f3)\",\n    secondaryBg: \"linear-gradient(90deg, #ff4d4d, #f9cb28, #ff0080)\",\n    tertiaryBg: \"linear-gradient(90deg, #0070f3, #00d4ff, #7928ca)\",\n  },\n  success: {\n    primaryBg:\n      \"linear-gradient(90deg, #047857, #059669, #10b981, #14b8a6, #0d9488)\",\n    secondaryBg: \"linear-gradient(90deg, #34d399, #6ee7b7, #2dd4bf, #5eead4)\",\n    tertiaryBg: \"linear-gradient(90deg, #065f46, #047857, #0f766e)\",\n  },\n  destructive: {\n    primaryBg:\n      \"linear-gradient(90deg, #991b1b, #dc2626, #e11d48, #f43f5e, #be123c)\",\n    secondaryBg: \"linear-gradient(90deg, #fb7185, #fda4af, #f87171, #fecdd3)\",\n    tertiaryBg: \"linear-gradient(90deg, #7f1d1d, #b91c1c, #dc2626)\",\n  },\n}\n\nfunction badgeBorderGlowConfig(\n  translucent: boolean,\n  rimEmphasis: boolean,\n  rimVariant: RimVariant = \"default\"\n) {\n  let idleOpacity: number\n  if (translucent) {\n    idleOpacity = rimEmphasis ? 0.92 : 0.72\n  } else {\n    idleOpacity = rimEmphasis ? 0.78 : 0.5\n  }\n  const gradients = RIM_BLOB_GRADIENTS[rimVariant]\n  return {\n    opacityIdle: idleOpacity,\n    primaryBg: gradients.primaryBg,\n    secondaryBg: gradients.secondaryBg,\n    tertiaryBg: gradients.tertiaryBg,\n    primaryLeft: [\"-5%\", \"75%\", \"-5%\"],\n    secondaryLeft: [\"65%\", \"10%\", \"65%\"],\n    tertiaryLeft: [\"25%\", \"55%\", \"25%\"],\n    durationMain: 6,\n    durationSec: 5,\n    durationTer: 4,\n  }\n}\n\nfunction badgeFillBackgroundFor(\n  translucent: boolean,\n  interactive: boolean,\n  rimEmphasis: boolean\n) {\n  if (translucent) {\n    if (interactive && rimEmphasis) {\n      return \"linear-gradient(to bottom, color-mix(in oklch, var(--card) 88%, transparent) 0%, color-mix(in oklch, var(--card) 74%, transparent) 100%)\"\n    }\n    return \"linear-gradient(to bottom, color-mix(in oklch, var(--card) 82%, transparent) 0%, color-mix(in oklch, var(--card) 70%, transparent) 100%)\"\n  }\n  if (interactive && rimEmphasis) {\n    return \"linear-gradient(to bottom, var(--card) 0%, color-mix(in oklch, var(--card) 90%, var(--muted)) 100%)\"\n  }\n  return \"linear-gradient(to bottom, var(--card) 0%, color-mix(in oklch, var(--card) 94%, var(--muted)) 100%)\"\n}\n\nconst layoutTransition = {\n  type: \"spring\" as const,\n  bounce: 0,\n  duration: 0.32,\n}\n\n/** Variants that use gradient rim + frosted surface (ghost/link stay minimal). */\nconst CHROMED_BADGE_VARIANTS = new Set<\n  NonNullable<VariantProps<typeof badgeVariants>[\"variant\"]>\n>([\"default\", \"secondary\", \"destructive\", \"outline\"])\n\nfunction badgeRimVariant(\n  variant: NonNullable<VariantProps<typeof badgeVariants>[\"variant\"]>\n): RimVariant {\n  if (variant === \"destructive\") {\n    return \"destructive\"\n  }\n  return \"default\"\n}\n\n/**\n * Same ambient underglow as {@link HaloSwitch} — radial brand ramps clipped to the pill\n * so it reads as depth, not a second misaligned rim.\n */\nfunction BadgeAmbientUnderglow({ reduceMotion }: { reduceMotion: boolean }) {\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[9999px]\">\n      <div\n        className={cn(\n          \"absolute inset-0\",\n          reduceMotion ? \"opacity-[0.38]\" : \"opacity-50\"\n        )}\n      >\n        <div\n          className=\"absolute inset-0 blur-2xl\"\n          style={{\n            background:\n              \"radial-gradient(90% 70% at 50% 100%, rgba(255,0,128,0.28), rgba(121,40,202,0.12) 48%, transparent 62%)\",\n          }}\n        />\n        <div\n          className=\"absolute inset-0 opacity-70 blur-xl dark:opacity-50\"\n          style={{\n            background:\n              \"radial-gradient(70% 55% at 30% 0%, rgba(0,212,255,0.22), rgba(0,112,243,0.08) 45%, transparent 58%)\",\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n\n/** Scaled pill blobs for `h-5` badges — same motion keys as `GradientRimPillBlobs`. */\nfunction BadgeRimPillBlobs({\n  borderGlow,\n  reduceMotion,\n}: {\n  borderGlow: ReturnType<typeof badgeBorderGlowConfig>\n  reduceMotion: boolean\n}) {\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-full\">\n      <motion.div\n        animate={{\n          opacity: reduceMotion\n            ? borderGlow.opacityIdle * 0.82\n            : borderGlow.opacityIdle,\n        }}\n        className=\"absolute inset-x-0 bottom-0 h-1/2\"\n        transition={{ duration: 0.25, ease: \"easeOut\" }}\n      >\n        <motion.div\n          animate={{\n            left: borderGlow.primaryLeft,\n          }}\n          className=\"-bottom-2 absolute h-10 w-28 blur-xl\"\n          style={{\n            background: borderGlow.primaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationMain,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n        <motion.div\n          animate={{\n            left: borderGlow.secondaryLeft,\n          }}\n          className=\"-bottom-1.5 absolute h-8 w-20 blur-lg\"\n          style={{\n            background: borderGlow.secondaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationSec,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n        <motion.div\n          animate={{\n            left: borderGlow.tertiaryLeft,\n          }}\n          className=\"-bottom-1 absolute h-6 w-16 blur-md\"\n          style={{\n            background: borderGlow.tertiaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationTer,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n      </motion.div>\n    </div>\n  )\n}\n\nconst chromedLabelClasses: Record<\n  \"default\" | \"secondary\" | \"destructive\" | \"outline\",\n  string\n> = {\n  default: \"text-primary\",\n  secondary: \"text-muted-foreground\",\n  destructive:\n    \"text-destructive aria-invalid:text-destructive dark:aria-invalid:text-destructive\",\n  outline: \"text-foreground\",\n}\n\n/** Border tints (same `color-mix` system as `HaloSwitch` shell). */\nconst chromedBorderClasses: Record<\n  \"default\" | \"secondary\" | \"destructive\" | \"outline\",\n  string\n> = {\n  default:\n    \"border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))]\",\n  secondary:\n    \"border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))]\",\n  destructive:\n    \"border-[color-mix(in_oklch,var(--border)_65%,oklch(0.86_0.09_22))]\",\n  outline:\n    \"border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))]\",\n}\n\n/** Plain string/number children only — used to pick a perfect circle for single-digit counts. */\nfunction plainChildLength(children: ReactNode): number | null {\n  if (typeof children === \"string\" || typeof children === \"number\") {\n    return String(children).length\n  }\n  return null\n}\n\n/** Explicit transform in transition (never `transition: all`). Easing matches cross-fade polish in the design skill. */\nconst chromedSurfaceBase =\n  \"relative z-[1] flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full px-2 font-medium text-[0.625rem] leading-none tracking-tight antialiased outline-none transition-[background-color,backdrop-filter,border-color,box-shadow,transform] duration-200 ease-[cubic-bezier(0.2,0,0,1)] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5\"\n\nfunction chromedShapeClasses(\n  singleDigitCircle: boolean,\n  multiDigitTabular: boolean\n): string {\n  if (singleDigitCircle) {\n    return \"size-5 px-0\"\n  }\n  if (multiDigitTabular) {\n    return \"h-5 min-w-5 px-1.5\"\n  }\n  return \"h-5 px-2\"\n}\n\nexport interface HaloBadgeProps\n  extends Omit<ComponentProps<typeof motion.span>, \"children\">,\n    VariantProps<typeof badgeVariants> {\n  children?: ReactNode\n  /** Leading “live” dot — opacity pulse only (respects reduced motion). */\n  live?: boolean\n  /**\n   * Animate width when label/children change. Prefer off for badges that update very frequently.\n   * @default true\n   */\n  layout?: boolean\n  /** Avoids digit shift when children are numeric strings. */\n  tabularNums?: boolean\n  /** Frosted inner surface so gradient blobs read through (matches other animated components). */\n  translucent?: boolean\n  /** Stronger rim + fill on hover (matches `HaloCard` / pill buttons). */\n  interactive?: boolean\n  /**\n   * Subtle `scale(0.96)` on press when `interactive` (interruptible CSS transition).\n   * @default true\n   */\n  pressScale?: boolean\n}\n\nfunction HaloBadgeChrome({\n  borderGlow,\n  body,\n  chromedKey,\n  className,\n  enableLayout,\n  fillBackground,\n  interactive,\n  live,\n  onMouseEnter,\n  onMouseLeave,\n  pressScale,\n  pulseTransition,\n  reduceMotion,\n  singleDigitCircle,\n  multiDigitTabular,\n  translucent,\n  ...props\n}: {\n  borderGlow: ReturnType<typeof badgeBorderGlowConfig>\n  body: ReactNode\n  chromedKey: keyof typeof chromedLabelClasses\n  className?: string\n  enableLayout: boolean\n  fillBackground: string\n  interactive: boolean\n  live: boolean\n  onMouseEnter?: ComponentProps<typeof motion.span>[\"onMouseEnter\"]\n  onMouseLeave?: ComponentProps<typeof motion.span>[\"onMouseLeave\"]\n  pressScale: boolean\n  pulseTransition: { duration: number; ease: \"easeInOut\"; repeat: number }\n  reduceMotion: boolean\n  singleDigitCircle: boolean\n  multiDigitTabular: boolean\n  translucent: boolean\n} & Omit<ComponentProps<typeof motion.span>, \"children\">) {\n  const shapeClasses = chromedShapeClasses(singleDigitCircle, multiDigitTabular)\n\n  return (\n    <motion.span\n      className={cn(\n        \"relative inline-flex max-w-full items-center self-center leading-none\",\n        className\n      )}\n      data-slot=\"halo-badge\"\n      layout={enableLayout}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      transition={layoutTransition}\n      {...props}\n    >\n      {translucent ? (\n        <BadgeAmbientUnderglow reduceMotion={reduceMotion} />\n      ) : null}\n      {/*\n        Inline baseline/line-height on the rim wrapper skews the inner pill inside `p-px`.\n        `inline-flex` + `leading-none` + `text-[0]` keeps the ring concentric; text size is set on the inner surface.\n      */}\n      {/*\n        Concentric radii: outer `rounded-full` + `p-px` (1px) with inner `rounded-full`\n        reads as a pill ring; both use full rounding so caps stay semicircular.\n      */}\n      <span className=\"relative z-[1] inline-flex items-center justify-center rounded-full p-px text-[0] leading-none\">\n        <BadgeRimPillBlobs\n          borderGlow={borderGlow}\n          reduceMotion={reduceMotion}\n        />\n        <span\n          className={cn(\n            chromedSurfaceBase,\n            chromedLabelClasses[chromedKey],\n            shapeClasses,\n            interactive && \"touch-manipulation\",\n            interactive &&\n              pressScale &&\n              !reduceMotion &&\n              \"active:scale-[0.96] motion-reduce:active:scale-100\",\n            /* Shell border + shadows aligned with `HaloSwitch` outer shell */\n            \"border\",\n            chromedBorderClasses[chromedKey],\n            \"shadow-[0_1px_0_rgba(255,255,255,0.55)_inset,0_1px_2px_rgba(0,0,0,0.03),0_6px_18px_rgba(0,0,0,0.04)]\",\n            \"dark:border-border/80 dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_1px_2px_rgba(0,0,0,0.28),0_6px_22px_rgba(0,0,0,0.32)]\",\n            interactive &&\n              \"fine-hover:hover:border-ring/50 fine-hover:hover:shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_1px_3px_rgba(0,0,0,0.05),0_8px_22px_rgba(0,0,0,0.06)]\",\n            interactive &&\n              \"dark:fine-hover:hover:border-zinc-500/60 dark:fine-hover:hover:shadow-[0_1px_0_rgba(255,255,255,0.08)_inset,0_2px_4px_rgba(0,0,0,0.38),0_10px_28px_rgba(0,0,0,0.42)]\",\n            translucent && \"backdrop-blur-xl backdrop-saturate-150\"\n          )}\n          style={{ background: fillBackground }}\n        >\n          {live ? (\n            <motion.span\n              animate={\n                reduceMotion ? { opacity: 1 } : { opacity: [0.35, 1, 0.35] }\n              }\n              aria-hidden\n              className=\"size-1.5 shrink-0 translate-y-[0.5px] self-center rounded-full bg-current opacity-90\"\n              transition={pulseTransition}\n            />\n          ) : null}\n          {body}\n        </span>\n      </span>\n    </motion.span>\n  )\n}\n\nexport function HaloBadge({\n  variant = \"default\",\n  className,\n  children,\n  live = false,\n  layout = true,\n  tabularNums = false,\n  translucent = true,\n  interactive = true,\n  pressScale = true,\n  onMouseEnter,\n  onMouseLeave,\n  ...props\n}: HaloBadgeProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const enableLayout = layout && !reduceMotion\n  const [isHovered, setIsHovered] = useState(false)\n\n  const pulseTransition = useMemo(\n    () => ({\n      duration: reduceMotion ? 0 : 1.15,\n      ease: \"easeInOut\" as const,\n      repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n    }),\n    [reduceMotion]\n  )\n\n  const body =\n    tabularNums || live ? (\n      <span\n        className={cn(\n          \"inline-flex items-center leading-none\",\n          tabularNums && \"font-variant-numeric tabular-nums tracking-tight\",\n          live && \"min-w-[1ch]\"\n        )}\n      >\n        {children}\n      </span>\n    ) : (\n      <span className=\"inline-flex items-center leading-none\">{children}</span>\n    )\n\n  const resolvedVariant = variant ?? (\"default\" as NonNullable<typeof variant>)\n  const useChrome = CHROMED_BADGE_VARIANTS.has(resolvedVariant)\n  const rimVariant = badgeRimVariant(resolvedVariant)\n  const rimEmphasis = interactive && isHovered\n\n  const digitLen = plainChildLength(children)\n  const singleDigitCircle = Boolean(tabularNums) && digitLen === 1 && !live\n  const multiDigitTabular =\n    Boolean(tabularNums) && !live && digitLen !== null && digitLen > 1\n\n  const borderGlow = useMemo(\n    () => badgeBorderGlowConfig(translucent, rimEmphasis, rimVariant),\n    [translucent, rimEmphasis, rimVariant]\n  )\n\n  const fillBackground = useMemo(\n    () => badgeFillBackgroundFor(translucent, interactive, rimEmphasis),\n    [translucent, interactive, rimEmphasis]\n  )\n\n  if (!useChrome) {\n    return (\n      <motion.span\n        className={cn(badgeVariants({ variant }), \"relative\", className)}\n        layout={enableLayout}\n        transition={layoutTransition}\n        {...props}\n      >\n        {live ? (\n          <motion.span\n            animate={\n              reduceMotion ? { opacity: 1 } : { opacity: [0.35, 1, 0.35] }\n            }\n            aria-hidden\n            className=\"size-1.5 shrink-0 translate-y-[0.5px] self-center rounded-full bg-current opacity-90\"\n            transition={pulseTransition}\n          />\n        ) : null}\n        {body}\n      </motion.span>\n    )\n  }\n\n  const chromedKey = resolvedVariant as keyof typeof chromedLabelClasses\n\n  return (\n    <HaloBadgeChrome\n      body={body}\n      borderGlow={borderGlow}\n      chromedKey={chromedKey}\n      className={className}\n      enableLayout={enableLayout}\n      fillBackground={fillBackground}\n      interactive={interactive}\n      live={live}\n      multiDigitTabular={multiDigitTabular}\n      onMouseEnter={(e) => {\n        setIsHovered(true)\n        onMouseEnter?.(e)\n      }}\n      onMouseLeave={(e) => {\n        setIsHovered(false)\n        onMouseLeave?.(e)\n      }}\n      pressScale={pressScale}\n      pulseTransition={pulseTransition}\n      reduceMotion={reduceMotion}\n      singleDigitCircle={singleDigitCircle}\n      translucent={translucent}\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}