{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "badge",
  "title": "Badge",
  "description": "A compact status label with solid, muted, and dot variants for tagging content inline.",
  "dependencies": [
    "class-variance-authority",
    "@radix-ui/react-slot"
  ],
  "files": [
    {
      "path": "components/ui/badge.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  forwardRef,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type MouseEventHandler,\n  type ReactNode,\n} from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { motion, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nconst badgeVariants = cva('inline-flex items-center squircle font-medium whitespace-nowrap', {\n  variants: {\n    variant: {\n      solid: 'border border-(--color-border) bg-(--color-surface) text-(--color-fg)',\n      muted: 'border border-(--color-border)/60 bg-(--color-surface-2) text-(--color-muted)',\n      dot: 'border border-(--color-border) bg-(--color-bg) text-(--color-muted)',\n    },\n    size: {\n      sm: 'h-5.5 px-2.5 text-[11px] gap-1',\n      md: 'h-6.5 px-3 text-[12px] gap-1.5',\n      lg: 'h-8 px-3.5 text-[13px] gap-1.5',\n    },\n    interactive: {\n      true: 'cursor-pointer transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg)',\n      false: '',\n    },\n  },\n  defaultVariants: {\n    variant: 'solid',\n    size: 'md',\n    interactive: false,\n  },\n})\n\nexport interface BadgeProps\n  extends\n    Omit<HTMLAttributes<HTMLSpanElement>, 'color'>,\n    Omit<VariantProps<typeof badgeVariants>, 'interactive'> {\n  icon?: ReactNode\n  onDismiss?: MouseEventHandler<HTMLButtonElement>\n  dismissLabel?: string\n  pulse?: boolean\n  shimmer?: boolean\n  asChild?: boolean\n}\n\nconst dotSizeFor = (size: BadgeProps['size']) => (size === 'sm' ? 6 : size === 'lg' ? 8 : 7)\n\nconst dismissSizeFor = (size: BadgeProps['size']) =>\n  size === 'lg' ? 'size-4 [&_svg]:size-3' : 'size-3.5 [&_svg]:size-2.5'\n\nexport const Badge = forwardRef<HTMLSpanElement, BadgeProps>(\n  (\n    {\n      className,\n      variant = 'solid',\n      size = 'md',\n      icon,\n      onDismiss,\n      dismissLabel = 'Remove',\n      pulse = false,\n      shimmer = true,\n      asChild = false,\n      onClick,\n      onKeyDown,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion()\n    const isDot = variant === 'dot'\n    const isClickable = Boolean(onClick)\n    const shouldPulse = isDot && pulse && !reduceMotion\n    const shouldShimmer = variant === 'solid' && shimmer && !reduceMotion\n    const dotSize = dotSizeFor(size)\n\n    const rootClassName = cn(\n      badgeVariants({ variant, size, interactive: isClickable }),\n      shouldShimmer && 'relative overflow-hidden',\n      className,\n    )\n\n    if (asChild) {\n      return (\n        <Slot ref={ref} className={rootClassName} onClick={onClick} {...props}>\n          {children}\n        </Slot>\n      )\n    }\n\n    const handleKeyDown = (e: KeyboardEvent<HTMLSpanElement>) => {\n      onKeyDown?.(e)\n      if (e.defaultPrevented || !isClickable) return\n      if (e.key === 'Enter' || e.key === ' ') {\n        e.preventDefault()\n        onClick?.(e as unknown as React.MouseEvent<HTMLSpanElement>)\n      }\n    }\n\n    const DotEl = shouldPulse ? motion.span : 'span'\n\n    return (\n      <span\n        ref={ref}\n        className={rootClassName}\n        onMouseEnter={() => isClickable && playHoverSound()}\n        onClick={(e) => {\n          if (isClickable) playClickSound()\n          onClick?.(e)\n        }}\n        onKeyDown={isClickable || onKeyDown ? handleKeyDown : undefined}\n        {...(isClickable ? { role: 'button', tabIndex: 0 } : {})}\n        {...props}\n      >\n        {shouldShimmer && (\n          <motion.span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n            style={{\n              background:\n                'linear-gradient(90deg, transparent 0%, color-mix(in srgb, currentColor 30%, transparent) 50%, transparent 100%)',\n            }}\n            animate={{ x: ['-100%', '200%'] }}\n            transition={{ duration: 1.8, repeat: Infinity, repeatDelay: 0.8, ease: 'easeInOut' }}\n          />\n        )}\n        {isDot && (\n          <DotEl\n            aria-hidden=\"true\"\n            className=\"shrink-0 rounded-full bg-muted-foreground\"\n            style={{ width: dotSize, height: dotSize }}\n            {...(shouldPulse\n              ? {\n                  animate: { opacity: [0.5, 1, 0.5] },\n                  transition: { duration: 1.8, repeat: Infinity, ease: 'easeInOut' },\n                }\n              : {})}\n          />\n        )}\n        {!isDot && icon && (\n          <span aria-hidden=\"true\" className=\"inline-flex shrink-0 [&_svg]:size-3\">\n            {icon}\n          </span>\n        )}\n        <span className=\"min-w-0 truncate\">{children}</span>\n        {onDismiss && (\n          <button\n            type=\"button\"\n            aria-label={dismissLabel}\n            className={cn(\n              'relative -me-0.5 inline-flex shrink-0 items-center justify-center rounded-full text-current/70 transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none hover:text-current focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--color-accent)',\n              dismissSizeFor(size),\n            )}\n            onMouseEnter={() => playHoverSound()}\n            onClick={(e) => {\n              playClickSound()\n              e.stopPropagation()\n              onDismiss(e)\n            }}\n          >\n            <svg aria-hidden=\"true\" fill=\"none\" viewBox=\"0 0 12 12\">\n              <path\n                d=\"M3 3l6 6M9 3 3 9\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeWidth=\"1.75\"\n              />\n            </svg>\n          </button>\n        )}\n      </span>\n    )\n  },\n)\n\nBadge.displayName = 'Badge'\n\nexport { badgeVariants }\n\nexport function BadgePreview() {\n  return (\n    <div className=\"flex w-full flex-col items-center gap-10 p-8\">\n      <p className=\"flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-balance font-medium text-lg leading-snug tracking-tight text-foreground sm:text-xl\">\n        <span>This update is</span>\n        <span className=\"inline-flex translate-y-px align-middle\">\n          <Badge>Early Access</Badge>\n        </span>\n        <span>and ready to ship.</span>\n      </p>\n\n      <div className=\"flex flex-wrap items-center justify-center gap-3\">\n        <Badge size=\"sm\">Small</Badge>\n        <Badge size=\"md\">Medium</Badge>\n        <Badge size=\"lg\">Large</Badge>\n      </div>\n\n      <div className=\"flex flex-wrap items-center justify-center gap-3\">\n        <Badge>Solid</Badge>\n        <Badge variant=\"muted\">Muted</Badge>\n        <Badge variant=\"dot\" pulse>\n          Live\n        </Badge>\n      </div>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/sound.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nconst MUTE_KEY = 'sound-muted'\nconst MUTE_EVENT = 'sound-mute-change'\n\nlet muted = false\nif (typeof window !== 'undefined') {\n  try {\n    muted = localStorage.getItem(MUTE_KEY) === '1'\n  } catch {}\n}\n\nexport function isSoundMuted(): boolean {\n  return muted\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value\n  if (typeof window !== 'undefined') {\n    try {\n      localStorage.setItem(MUTE_KEY, value ? '1' : '0')\n    } catch {}\n    window.dispatchEvent(new CustomEvent(MUTE_EVENT))\n  }\n}\n\nexport function toggleSoundMuted(): boolean {\n  setSoundMuted(!muted)\n  return muted\n}\n\nexport function subscribeSoundMuted(callback: () => void) {\n  if (typeof window === 'undefined') return () => {}\n  window.addEventListener(MUTE_EVENT, callback)\n  const onStorage = (e: StorageEvent) => {\n    if (e.key === MUTE_KEY) callback()\n  }\n  window.addEventListener('storage', onStorage)\n  return () => {\n    window.removeEventListener(MUTE_EVENT, callback)\n    window.removeEventListener('storage', onStorage)\n  }\n}\n\nlet audioCtx: AudioContext | null = null\n\nfunction initAudio(): AudioContext | null {\n  if (typeof window === 'undefined') return null\n  if (!audioCtx) {\n    const AudioContextClass =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    if (AudioContextClass) {\n      audioCtx = new AudioContextClass()\n    }\n  }\n  if (audioCtx && audioCtx.state === 'suspended') {\n    audioCtx.resume().catch(() => {})\n  }\n  return audioCtx\n}\n\nif (typeof window !== 'undefined') {\n  const unlock = () => {\n    initAudio()\n    window.removeEventListener('pointerdown', unlock)\n    window.removeEventListener('keydown', unlock)\n    window.removeEventListener('touchstart', unlock)\n  }\n  window.addEventListener('pointerdown', unlock, { passive: true })\n  window.addEventListener('keydown', unlock, { passive: true })\n  window.addEventListener('touchstart', unlock, { passive: true })\n}\n\nexport function getAudioContext(): AudioContext | null {\n  return initAudio()\n}\n\nexport function playHoverSound(volume = 0.12, pitch = 1.2) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1400 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(350 * pitch, now + 0.012)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.012)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.012)\n  } catch {}\n}\n\nexport function playClickSound(volume = 0.2, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(850 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(160 * pitch, now + 0.02)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.02)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.02)\n  } catch {}\n}\n\nexport function playTickSound(volume = 0.1, pitch = 1.5) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1800 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(450 * pitch, now + 0.01)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.01)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.01)\n  } catch {}\n}\n\nexport function playBounceSound(volume = 0.3, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(260 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(650 * pitch, now + 0.04)\n    osc.frequency.exponentialRampToValueAtTime(190 * pitch, now + 0.14)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.14)\n\n    const subOsc = ctx.createOscillator()\n    const subGain = ctx.createGain()\n\n    subOsc.type = 'triangle'\n    subOsc.frequency.setValueAtTime(420 * pitch, now)\n    subOsc.frequency.exponentialRampToValueAtTime(120 * pitch, now + 0.05)\n\n    subGain.gain.setValueAtTime(volume * 0.7, now)\n    subGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05)\n\n    subOsc.connect(subGain)\n    subGain.connect(ctx.destination)\n\n    subOsc.start(now)\n    subOsc.stop(now + 0.05)\n  } catch {}\n}\n"
    }
  ],
  "type": "registry:ui"
}
