{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "A pressable trigger with spring-animated depth feedback, sound cues, and solid/outline/ghost/link variants.",
  "dependencies": [
    "motion",
    "@radix-ui/react-slot",
    "class-variance-authority"
  ],
  "files": [
    {
      "path": "components/ui/button.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { animate, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\nimport { playClickSound, playHoverSound } from '@/lib/sound'\n\nexport const buttonVariants = cva(\n  [\n    'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium',\n    'rounded-lg squircle-corners',\n    'select-none',\n    'transition-colors duration-(--motion-dur-fast) ease-(--motion-ease-out)',\n    'motion-reduce:transition-none motion-reduce:transform-none',\n    'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)',\n    'focus-visible:outline-none',\n    'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',\n    'aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50',\n  ].join(' '),\n  {\n    variants: {\n      variant: {\n        solid: 'bg-(--color-fg) text-(--color-bg) hover:bg-(--color-fg)/90',\n        outline: 'border border-(--color-border) text-(--color-fg) hover:bg-(--color-surface)',\n        ghost: 'text-(--color-fg) hover:bg-(--color-surface-2)',\n        link: 'text-(--color-fg) underline-offset-4 hover:underline',\n        destructive:\n          'bg-(--color-error) text-(--color-bg) hover:bg-(--color-error)/90 focus-visible:ring-(--color-error)',\n      },\n      size: {\n        sm: 'min-h-9 px-3 text-xs',\n        md: 'min-h-11 px-4 text-sm',\n        lg: 'min-h-12 px-5 text-base',\n        'icon-sm': 'size-9 shrink-0 p-0',\n        icon: 'size-11 shrink-0 p-0',\n        'icon-lg': 'size-12 shrink-0 p-0',\n      },\n    },\n    defaultVariants: {\n      variant: 'solid',\n      size: 'md',\n    },\n  },\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n  loading?: boolean\n  /** Accessible label announced while an action is in progress. */\n  loadingLabel?: string\n  /** Replaces the default progress glyph without changing loading behavior. */\n  loadingIndicator?: React.ReactNode\n  /** @deprecated Use `variant=\"destructive\"`. */\n  destructive?: boolean\n  className?: string\n}\n\nfunction DefaultLoadingIndicator() {\n  return (\n    <svg\n      className=\"size-4 animate-spin motion-reduce:animate-none\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n        className=\"opacity-25\"\n      />\n      <path\n        d=\"M12 2a10 10 0 0 1 10 10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n        className=\"opacity-75\"\n      />\n    </svg>\n  )\n}\n\nfunction ButtonLoadingContent({\n  children,\n  indicator,\n}: {\n  children: React.ReactNode\n  indicator?: React.ReactNode\n}) {\n  return (\n    <span className=\"relative inline-flex min-w-0 items-center justify-center\">\n      <span className=\"invisible inline-flex items-center gap-2\" aria-hidden=\"true\">\n        {children}\n      </span>\n      <span className=\"absolute inset-0 flex items-center justify-center\">\n        {indicator ?? <DefaultLoadingIndicator />}\n      </span>\n    </span>\n  )\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      asChild = false,\n      loading = false,\n      loadingLabel,\n      loadingIndicator,\n      destructive = false,\n      disabled,\n      type = 'button',\n      tabIndex,\n      onClick,\n      onPointerDown,\n      onPointerUp,\n      onPointerCancel,\n      onPointerLeave,\n      onMouseEnter,\n      children,\n      style,\n      'aria-label': ariaLabel,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const internalRef = React.useRef<HTMLButtonElement | null>(null)\n    const prefersReducedMotion = useReducedMotion()\n    const isDisabled = disabled || loading\n    const resolvedVariant =\n      destructive && (variant === undefined || variant === 'solid') ? 'destructive' : variant\n    const destructiveClasses = destructive\n      ? variant === 'outline'\n        ? 'border-(--color-error) text-(--color-error) hover:bg-(--color-error)/10 focus-visible:ring-(--color-error)'\n        : variant === 'ghost'\n          ? 'text-(--color-error) hover:bg-(--color-error)/10 focus-visible:ring-(--color-error)'\n          : variant === 'link'\n            ? 'text-(--color-error) focus-visible:ring-(--color-error)'\n            : undefined\n      : undefined\n\n    const setRefs = React.useCallback(\n      (node: HTMLButtonElement | null) => {\n        internalRef.current = node\n        if (typeof forwardedRef === 'function') {\n          forwardedRef(node)\n        } else if (forwardedRef) {\n          forwardedRef.current = node\n        }\n      },\n      [forwardedRef],\n    )\n\n    const settle = React.useCallback(() => {\n      if (prefersReducedMotion) return\n      const element = internalRef.current\n      if (!element) return\n      animate(element, { transform: 'scale(1) translateY(0px)' }, { ...springs.settle })\n    }, [prefersReducedMotion])\n\n    const handlePointerDown = React.useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerDown?.(event)\n        if (event.defaultPrevented || isDisabled || prefersReducedMotion) return\n\n        const element = internalRef.current\n        if (!element) return\n        animate(element, { transform: 'scale(0.97) translateY(1px)' }, { ...springs.press })\n      },\n      [onPointerDown, isDisabled, prefersReducedMotion],\n    )\n\n    const handlePointerUp = React.useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerUp?.(event)\n        settle()\n      },\n      [onPointerUp, settle],\n    )\n\n    const handlePointerCancel = React.useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerCancel?.(event)\n        settle()\n      },\n      [onPointerCancel, settle],\n    )\n\n    const handlePointerLeave = React.useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerLeave?.(event)\n        settle()\n      },\n      [onPointerLeave, settle],\n    )\n\n    const handleClick = React.useCallback(\n      (event: React.MouseEvent<HTMLButtonElement>) => {\n        if (isDisabled) {\n          event.preventDefault()\n          event.stopPropagation()\n          return\n        }\n        playClickSound()\n        onClick?.(event)\n      },\n      [onClick, isDisabled],\n    )\n\n    const handleMouseEnter = React.useCallback(\n      (event: React.MouseEvent<HTMLButtonElement>) => {\n        if (!isDisabled) playHoverSound()\n        onMouseEnter?.(event)\n      },\n      [onMouseEnter, isDisabled],\n    )\n\n    const loadingContent = (content: React.ReactNode) => (\n      <ButtonLoadingContent indicator={loadingIndicator}>{content}</ButtonLoadingContent>\n    )\n\n    let renderedChildren: React.ReactNode = loading ? loadingContent(children) : children\n    if (asChild) {\n      const child = React.Children.only(children) as React.ReactElement<{\n        children?: React.ReactNode\n      }>\n      renderedChildren = React.cloneElement(\n        child,\n        undefined,\n        loading ? loadingContent(child.props.children) : child.props.children,\n      )\n    }\n\n    const Comp = asChild ? Slot : 'button'\n\n    return (\n      <Comp\n        ref={setRefs}\n        type={asChild ? undefined : type}\n        className={cn(\n          buttonVariants({ variant: resolvedVariant, size }),\n          destructiveClasses,\n          className,\n        )}\n        disabled={asChild ? undefined : isDisabled}\n        tabIndex={asChild && isDisabled ? -1 : tabIndex}\n        aria-label={loading && loadingLabel ? loadingLabel : ariaLabel}\n        aria-disabled={isDisabled || undefined}\n        aria-busy={loading || undefined}\n        data-loading={loading ? '' : undefined}\n        data-disabled={isDisabled ? '' : undefined}\n        data-variant={resolvedVariant ?? 'solid'}\n        data-size={size ?? 'md'}\n        onClick={handleClick}\n        onPointerDown={handlePointerDown}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerCancel}\n        onPointerLeave={handlePointerLeave}\n        onMouseEnter={handleMouseEnter}\n        {...props}\n        style={style}\n      >\n        {renderedChildren}\n      </Comp>\n    )\n  },\n)\n\nButton.displayName = 'Button'\n\nexport function ButtonPreview() {\n  return (\n    <div className=\"flex flex-wrap items-center justify-center gap-3 p-6\">\n      <Button variant=\"solid\">Solid</Button>\n      <Button variant=\"outline\">Outline</Button>\n      <Button variant=\"ghost\">Ghost</Button>\n      <Button variant=\"link\">Link</Button>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/motion-tokens.ts",
      "type": "registry:lib",
      "content": "export const durations = {\n  instant: '50ms',\n  fast: '120ms',\n  base: '200ms',\n  slow: '320ms',\n} as const\n\nexport const easings = {\n  out: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',\n} as const\n\nexport const springs = {\n  fast: { type: 'spring', stiffness: 400, damping: 30, mass: 0.8 },\n  press: { type: 'spring', stiffness: 700, damping: 32, mass: 1 },\n  moderate: { type: 'spring', stiffness: 300, damping: 24, mass: 1 },\n  settle: { type: 'spring', stiffness: 260, damping: 26, mass: 1 },\n} as const\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"
}
