{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "badge",
  "title": "Badge",
  "description": "Small status chip in five variants; the warm mise variant is reserved for the one item that must stand out.",
  "dependencies": [
    "radix-ui",
    "class-variance-authority",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/beam-border.json",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/badge.tsx",
      "content": "import type * as React from \"react\"\nimport { Slot } from \"radix-ui\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { BeamFrame, GlowFrame, GooeyCount, LiveDot, type BadgeEffect } from \"@/registry/miseui/ui/badge-effects\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst badgeVariants = cva(\n  [\n    \"inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium\",\n    \"transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-mise)\",\n    \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n    \"[&>svg]:pointer-events-none [&>svg]:size-3\",\n  ].join(\" \"),\n  {\n    variants: {\n      variant: {\n        default: \"border-transparent bg-primary text-primary-foreground\",\n        secondary: \"border-transparent bg-secondary text-secondary-foreground\",\n        destructive: \"border-transparent bg-destructive text-white focus-visible:ring-destructive/30\",\n        outline: \"text-foreground\",\n        mise: \"border-transparent bg-mise text-mise-foreground\",\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  },\n)\n\ntype BadgeProps = React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & {\n    asChild?: boolean\n    /** A living treatment on top of the variant. `count` needs text or number children and no asChild; otherwise it renders plain. */\n    effect?: BadgeEffect\n  }\n\n/**\n * Slottable lets the dot join a child element under asChild, where Slot expects exactly one child.\n * An array, not a fragment: Slot looks through arrays, but would pour its props onto a fragment.\n */\nfunction badgeContent(effect: BadgeEffect | undefined, asChild: boolean, variant: BadgeProps[\"variant\"], children: React.ReactNode) {\n  if (effect === \"live\") {\n    return [<LiveDot key=\"dot\" variant={variant} />, <Slot.Slottable key=\"label\">{children}</Slot.Slottable>]\n  }\n  if (effect === \"count\" && !asChild && (typeof children === \"number\" || typeof children === \"string\")) {\n    return <GooeyCount value={String(children)} />\n  }\n  return children\n}\n\nfunction Badge({ className, variant, asChild = false, effect, children, ...props }: BadgeProps) {\n  const Comp = asChild ? Slot.Root : \"span\"\n  const badge = (\n    <Comp\n      data-slot=\"badge\"\n      data-variant={variant ?? \"default\"}\n      data-effect={effect}\n      className={cn(badgeVariants({ variant }), effect === \"glow\" && \"border-mise/60\", className)}\n      {...props}\n    >\n      {badgeContent(effect, asChild, variant, children)}\n    </Comp>\n  )\n  if (effect === \"glow\") return <GlowFrame className={className}>{badge}</GlowFrame>\n  if (effect === \"beam\") return <BeamFrame variant={variant} className={className}>{badge}</BeamFrame>\n  return badge\n}\n\nexport { Badge, badgeVariants, type BadgeProps, type BadgeEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/badge-effects.tsx",
      "content": "import type * as React from \"react\"\nimport { useId, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { BeamBorder } from \"@/registry/miseui/ui/beam-border\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype BadgeEffect = \"glow\" | \"beam\" | \"count\" | \"live\"\n\nconst BEAM_LENGTH_PX = 40\nconst BEAM_LAP_SECONDS = 3\nconst BREATH_TRANSITION = { duration: 2.4, ease: \"easeInOut\", repeat: Infinity, repeatType: \"mirror\" } as const\nconst STILL_TRANSITION = { duration: 0 } as const\nconst REDUCED_FADE = { duration: 0.15 } as const\n/** Far enough for the old and new glyph to overlap only near the middle of the swap, where the goo bridges them. */\nconst DIGIT_TRAVEL_PX = 7\nconst DIGIT_BLUR_PX = 2.5\n/** Tuned for 12 px type: blur past a stem's width, then an alpha ramp steep enough to re-solidify it. */\nconst GOO_BLUR = 1\nconst GOO_ALPHA_SLOPE = 12\nconst GOO_ALPHA_OFFSET = -4.5\n\n/** A radius on the badge has to reach the frame too, or the halo and the beam would trace another corner. */\nfunction frameRadius(className: string | undefined) {\n  return (className ?? \"\").split(/\\s+/).filter((token) => token.startsWith(\"rounded\"))\n}\n\nfunction GlowFrame({ className, children }: { className: string | undefined; children: React.ReactNode }) {\n  const reduceMotion = useReducedMotion()\n  return (\n    <span data-slot=\"badge-glow\" className={cn(\"relative isolate inline-flex w-fit rounded-md\", frameRadius(className))}>\n      <motion.span\n        aria-hidden=\"true\"\n        data-testid=\"badge-halo\"\n        data-reduced={reduceMotion ? \"true\" : \"false\"}\n        // Transform and opacity keep the loop on the compositor; animating box-shadow would repaint every frame.\n        className=\"absolute -inset-1 -z-10 rounded-[inherit] bg-mise blur-md [@media(prefers-reduced-transparency:reduce)]:hidden\"\n        initial={false}\n        animate={\n          reduceMotion\n            ? { opacity: 0.6, transform: \"scale(1)\" }\n            : { opacity: [0.35, 0.9], transform: [\"scale(0.94)\", \"scale(1.1)\"] }\n        }\n        transition={reduceMotion ? STILL_TRANSITION : BREATH_TRANSITION}\n      />\n      {children}\n    </span>\n  )\n}\n\ninterface BeamFrameProps {\n  variant: string | null | undefined\n  className: string | undefined\n  children: React.ReactNode\n}\n\nfunction BeamFrame({ variant, className, children }: BeamFrameProps) {\n  return (\n    <BeamBorder\n      // A warm beam on a warm surface would vanish.\n      color={variant === \"mise\" ? \"foreground\" : \"mise\"}\n      size={BEAM_LENGTH_PX}\n      duration={BEAM_LAP_SECONDS}\n      className={cn(\"inline-flex w-fit rounded-md\", frameRadius(className))}\n    >\n      {children}\n    </BeamBorder>\n  )\n}\n\nfunction LiveDot({ variant }: { variant: string | null | undefined }) {\n  const reduceMotion = useReducedMotion()\n  // On the warm and the red surface a mise dot would not read, so it takes the ink colour.\n  const tone = variant === \"mise\" || variant === \"destructive\" ? \"bg-current\" : \"bg-mise\"\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-testid=\"badge-live-dot\"\n      data-reduced={reduceMotion ? \"true\" : \"false\"}\n      className=\"relative flex size-1.5 shrink-0\"\n    >\n      <motion.span\n        className={cn(\"absolute inset-0 rounded-full\", tone)}\n        initial={false}\n        animate={\n          reduceMotion\n            ? { opacity: 0.35, transform: \"scale(1.8)\" }\n            : { opacity: [0.6, 0.1], transform: [\"scale(1)\", \"scale(2.6)\"] }\n        }\n        transition={reduceMotion ? STILL_TRANSITION : BREATH_TRANSITION}\n      />\n      <span className={cn(\"relative size-full rounded-full\", tone)} />\n    </span>\n  )\n}\n\ninterface GooeyDigitProps {\n  digit: string\n  filterId: string\n  /** False for digits present at first paint, which must not replay an arrival. */\n  enter: boolean\n  reduceMotion: boolean\n}\n\nfunction hiddenDigit(offset: number) {\n  return { opacity: 0, y: offset, filter: `blur(${DIGIT_BLUR_PX}px)` }\n}\n\n/**\n * The goo filter only runs while a swap is in flight. Blur-then-threshold rounds small type, so leaving it on\n * would soften the resting number. Each digit owns the flag, so unchanged digits stay crisp next to a moving one.\n */\nfunction GooeyDigit({ digit, filterId, enter, reduceMotion }: GooeyDigitProps) {\n  const [settled, setSettled] = useState(digit)\n  const morphing = !reduceMotion && settled !== digit\n  return (\n    <span\n      data-testid=\"badge-digit\"\n      data-morphing={morphing ? \"true\" : \"false\"}\n      className=\"inline-grid\"\n      style={morphing ? { filter: `url(#${filterId})` } : undefined}\n    >\n      <AnimatePresence initial={enter} onExitComplete={() => setSettled(digit)}>\n        <motion.span\n          key={digit}\n          className=\"col-start-1 row-start-1\"\n          initial={reduceMotion ? { opacity: 0 } : hiddenDigit(DIGIT_TRAVEL_PX)}\n          animate={reduceMotion ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={reduceMotion ? { opacity: 0 } : hiddenDigit(-DIGIT_TRAVEL_PX)}\n          transition={reduceMotion ? REDUCED_FADE : SPRING_DEFAULT}\n        >\n          {digit}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  )\n}\n\nfunction DigitGooFilter({ id }: { id: string }) {\n  return (\n    <svg aria-hidden=\"true\" className=\"absolute size-0\">\n      <defs>\n        {/* The default filter region would clip a digit mid-travel. */}\n        <filter id={id} x=\"-50%\" y=\"-100%\" width=\"200%\" height=\"300%\" colorInterpolationFilters=\"sRGB\">\n          <feGaussianBlur in=\"SourceGraphic\" stdDeviation={GOO_BLUR} result=\"blur\" />\n          <feColorMatrix\n            in=\"blur\"\n            type=\"matrix\"\n            values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${GOO_ALPHA_SLOPE} ${GOO_ALPHA_OFFSET}`}\n          />\n        </filter>\n      </defs>\n    </svg>\n  )\n}\n\nfunction GooeyCount({ value }: { value: string }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const filterId = `badge-goo-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`\n  const [initialLength] = useState(value.length)\n  const digits = value.split(\"\")\n  return (\n    <>\n      <span className=\"sr-only\">{value}</span>\n      <span aria-hidden=\"true\" data-testid=\"badge-count\" className=\"inline-flex tabular-nums\">\n        {reduceMotion ? null : <DigitGooFilter id={filterId} />}\n        {digits.map((digit, index) => {\n          // Keyed from the right: units stay units when the number gains a digit.\n          const position = digits.length - index - 1\n          return (\n            <GooeyDigit\n              key={position}\n              digit={digit}\n              filterId={filterId}\n              enter={position >= initialLength}\n              reduceMotion={reduceMotion}\n            />\n          )\n        })}\n      </span>\n    </>\n  )\n}\n\nexport { BeamFrame, GlowFrame, GooeyCount, LiveDot, type BadgeEffect }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}