{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "action-button",
  "title": "Action Button",
  "description": "An async-aware button with an idle → pending → success/error state machine, promise tracking, abort support, and live screen-reader announcements.",
  "dependencies": [
    "motion",
    "@radix-ui/react-slot",
    "class-variance-authority"
  ],
  "files": [
    {
      "path": "components/ui/action-button.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { Button, type ButtonProps } from '@/components/ui/button'\n\nexport type ActionButtonState = 'idle' | 'pending' | 'success' | 'error'\n\nexport interface ActionButtonProps extends Omit<\n  ButtonProps,\n  'loading' | 'loadingLabel' | 'loadingIndicator' | 'onClick'\n> {\n  /**\n   * Controlled state. If provided, the component becomes fully controlled\n   * and ignores the result of `onAction`.\n   */\n  state?: ActionButtonState\n  /**\n   * Default state for uncontrolled mode.\n   */\n  defaultState?: ActionButtonState\n  /**\n   * A callback that returns a promise. The component tracks the promise lifecycle\n   * (idle → pending → success/error → idle). Receives an AbortSignal.\n   */\n  onAction?: (signal: AbortSignal) => Promise<void>\n  /**\n   * Callback fired whenever internal state changes (uncontrolled mode).\n   */\n  onStateChange?: (state: ActionButtonState) => void\n  /**\n   * Minimum time (ms) to show the pending state. Prevents flicker for fast operations.\n   * Defaults to 0 (no minimum). Opt-in only.\n   */\n  minPendingMs?: number\n  /**\n   * Time (ms) before resetting from success/error back to idle.\n   * Defaults to 2000ms. Set to 0 to disable auto-reset.\n   */\n  resetDelayMs?: number\n  /** Label content shown in idle state. */\n  idleLabel?: React.ReactNode\n  /** Label content shown in pending state. */\n  pendingLabel?: React.ReactNode\n  /** Label content shown in success state. */\n  successLabel?: React.ReactNode\n  /** Label content shown in error state. */\n  errorLabel?: React.ReactNode\n  /** Icon shown in idle state. */\n  idleIcon?: React.ReactNode\n  /** Icon shown in pending state. Defaults to a spinner. */\n  pendingIcon?: React.ReactNode\n  /** Icon shown in success state. Defaults to a checkmark. */\n  successIcon?: React.ReactNode\n  /** Icon shown in error state. Defaults to an × glyph. */\n  errorIcon?: React.ReactNode\n  /**\n   * Full control over rendering each state. Overrides individual label/icon props.\n   * The returned node is rendered inside the button.\n   */\n  renderState?: (state: ActionButtonState) => React.ReactNode\n  /**\n   * Accessible live-region announcement per state.\n   * Provide either a static string or a function.\n   */\n  announcements?: Partial<Record<ActionButtonState, string>>\n  /** onClick passthrough — fires only in idle state. Does not track async. */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>\n}\n\nfunction DefaultSpinnerIcon() {\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 DefaultCheckIcon() {\n  return (\n    <svg className=\"size-4\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n      <path\n        d=\"M5 13l4 4L19 7\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction DefaultErrorIcon() {\n  return (\n    <svg className=\"size-4\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n      <path\n        d=\"M6 6l12 12M6 18L18 6\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction StateLayer({\n  active,\n  children,\n  reducedMotion,\n  animateEnabled,\n}: {\n  active: boolean\n  children: React.ReactNode\n  reducedMotion: boolean\n  animateEnabled: boolean\n}) {\n  return (\n    <span\n      aria-hidden={!active}\n      className={cn(\n        'absolute inset-0 flex items-center justify-center gap-2',\n        reducedMotion || !animateEnabled\n          ? active\n            ? 'opacity-100'\n            : 'opacity-0'\n          : [\n              'transition-[opacity,transform,filter] duration-(--motion-dur-base) ease-(--motion-ease-in-out)',\n              active\n                ? 'scale-100 opacity-100 blur-0'\n                : 'pointer-events-none scale-95 opacity-0 blur-[1px]',\n            ],\n        !animateEnabled && !active && 'pointer-events-none',\n        'motion-reduce:transition-none motion-reduce:transform-none motion-reduce:filter-none',\n      )}\n    >\n      {children}\n    </span>\n  )\n}\n\nfunction useControlledActionState(\n  controlledState: ActionButtonState | undefined,\n  defaultState: ActionButtonState | undefined,\n  onStateChange: ((s: ActionButtonState) => void) | undefined,\n): [ActionButtonState, (s: ActionButtonState) => void] {\n  const [internalState, setInternalState] = React.useState<ActionButtonState>(\n    defaultState ?? 'idle',\n  )\n\n  const isControlled = controlledState !== undefined\n  const currentState = isControlled ? controlledState : internalState\n\n  const setState = React.useCallback(\n    (next: ActionButtonState) => {\n      if (!isControlled) {\n        setInternalState(next)\n      }\n      onStateChange?.(next)\n    },\n    [isControlled, onStateChange],\n  )\n\n  return [currentState, setState]\n}\n\nexport const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(\n  (\n    {\n      state: controlledState,\n      defaultState,\n      onAction,\n      onStateChange,\n      minPendingMs = 0,\n      resetDelayMs = 2000,\n      idleLabel,\n      pendingLabel,\n      successLabel,\n      errorLabel,\n      idleIcon,\n      pendingIcon,\n      successIcon,\n      errorIcon,\n      renderState,\n      announcements,\n      onClick,\n      asChild,\n      disabled,\n      className,\n      children,\n      ...buttonProps\n    },\n    forwardedRef,\n  ) => {\n    if (process.env.NODE_ENV !== 'production' && asChild) {\n      console.warn(\n        '[ActionButton] asChild is not supported — ActionButton manages its own internal' +\n          ' DOM layers for state crossfade. Ignoring asChild prop.',\n      )\n    }\n\n    const prefersReducedMotion = useReducedMotion() ?? false\n    const [currentState, setState] = useControlledActionState(\n      controlledState,\n      defaultState,\n      onStateChange,\n    )\n    const [animateEnabled, setAnimateEnabled] = React.useState(false)\n\n    React.useEffect(() => {\n      // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time post-mount reveal gate, not a sync loop: transitions must not run on the very first paint (avoids an initial-mount opacity flash), so this only needs to flip true once after hydration.\n      setAnimateEnabled(true)\n    }, [])\n\n    const abortControllerRef = React.useRef<AbortController | null>(null)\n    const resetTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const minPendingTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const pendingStartRef = React.useRef<number>(0)\n    const isMountedRef = React.useRef(true)\n    const isRunningRef = React.useRef(false)\n\n    React.useEffect(() => {\n      isMountedRef.current = true\n      return () => {\n        isMountedRef.current = false\n      }\n    }, [])\n\n    React.useEffect(() => {\n      return () => {\n        if (resetTimerRef.current !== null) {\n          clearTimeout(resetTimerRef.current)\n          resetTimerRef.current = null\n        }\n        if (minPendingTimerRef.current !== null) {\n          clearTimeout(minPendingTimerRef.current)\n          minPendingTimerRef.current = null\n        }\n        if (abortControllerRef.current) {\n          abortControllerRef.current.abort()\n          abortControllerRef.current = null\n        }\n      }\n    }, [])\n\n    const scheduleReset = React.useCallback(() => {\n      if (resetDelayMs <= 0) return\n      if (resetTimerRef.current !== null) {\n        clearTimeout(resetTimerRef.current)\n      }\n      resetTimerRef.current = setTimeout(() => {\n        resetTimerRef.current = null\n        if (!isMountedRef.current) return\n        setState('idle')\n      }, resetDelayMs)\n    }, [resetDelayMs, setState])\n\n    const transitionToTerminal = React.useCallback(\n      (terminal: 'success' | 'error') => {\n        const elapsed = Date.now() - pendingStartRef.current\n        const remaining = Math.max(0, minPendingMs - elapsed)\n\n        const applyTerminal = () => {\n          if (!isMountedRef.current) return\n          isRunningRef.current = false\n          setState(terminal)\n          scheduleReset()\n        }\n\n        if (remaining > 0) {\n          minPendingTimerRef.current = setTimeout(() => {\n            minPendingTimerRef.current = null\n            applyTerminal()\n          }, remaining)\n        } else {\n          applyTerminal()\n        }\n      },\n      [minPendingMs, setState, scheduleReset],\n    )\n\n    const handleClick = React.useCallback(\n      (event: React.MouseEvent<HTMLButtonElement>) => {\n        if (currentState !== 'idle') return\n\n        onClick?.(event)\n\n        if (!onAction || isRunningRef.current) return\n\n        isRunningRef.current = true\n\n        if (abortControllerRef.current) {\n          abortControllerRef.current.abort()\n        }\n\n        const controller = new AbortController()\n        abortControllerRef.current = controller\n\n        if (resetTimerRef.current !== null) {\n          clearTimeout(resetTimerRef.current)\n          resetTimerRef.current = null\n        }\n\n        pendingStartRef.current = Date.now()\n        setState('pending')\n\n        onAction(controller.signal).then(\n          () => {\n            if (controller.signal.aborted) return\n            transitionToTerminal('success')\n          },\n          (err: unknown) => {\n            if (controller.signal.aborted) return\n            // Don't treat AbortError as a real error\n            if (err instanceof DOMException && err.name === 'AbortError') return\n            transitionToTerminal('error')\n          },\n        )\n      },\n      [currentState, onClick, onAction, setState, transitionToTerminal],\n    )\n\n    const isDisabled = disabled || currentState === 'pending'\n\n    const resolveIdleContent = () => {\n      if (renderState) return renderState('idle')\n      return (\n        <>\n          {idleIcon}\n          {idleLabel ?? children}\n        </>\n      )\n    }\n\n    const resolvePendingContent = () => {\n      if (renderState) return renderState('pending')\n      return (\n        <>\n          {pendingIcon ?? <DefaultSpinnerIcon />}\n          {pendingLabel ?? idleLabel ?? children}\n        </>\n      )\n    }\n\n    const resolveSuccessContent = () => {\n      if (renderState) return renderState('success')\n      return (\n        <>\n          {successIcon ?? <DefaultCheckIcon />}\n          {successLabel ?? 'Done'}\n        </>\n      )\n    }\n\n    const resolveErrorContent = () => {\n      if (renderState) return renderState('error')\n      return (\n        <>\n          {errorIcon ?? <DefaultErrorIcon />}\n          {errorLabel ?? 'Failed'}\n        </>\n      )\n    }\n\n    const ariaLabel =\n      announcements?.[currentState] ?? (buttonProps['aria-label'] as string | undefined)\n\n    return (\n      <>\n        <Button\n          ref={forwardedRef}\n          {...buttonProps}\n          className={cn('relative overflow-hidden', className)}\n          disabled={isDisabled}\n          aria-disabled={isDisabled || undefined}\n          aria-busy={currentState === 'pending' || undefined}\n          aria-label={ariaLabel}\n          data-state={currentState}\n          onClick={handleClick}\n          asChild={false}\n        >\n          {/* Invisible sizing layer — grid-stacks all states so the widest determines width */}\n          <span className=\"invisible grid [&>*]:col-start-1 [&>*]:row-start-1\" aria-hidden=\"true\">\n            <span className=\"inline-flex items-center gap-2\">{resolveIdleContent()}</span>\n            <span className=\"inline-flex items-center gap-2\">{resolvePendingContent()}</span>\n            <span className=\"inline-flex items-center gap-2\">{resolveSuccessContent()}</span>\n            <span className=\"inline-flex items-center gap-2\">{resolveErrorContent()}</span>\n          </span>\n          <StateLayer\n            active={currentState === 'idle'}\n            reducedMotion={prefersReducedMotion}\n            animateEnabled={animateEnabled}\n          >\n            {resolveIdleContent()}\n          </StateLayer>\n          <StateLayer\n            active={currentState === 'pending'}\n            reducedMotion={prefersReducedMotion}\n            animateEnabled={animateEnabled}\n          >\n            {resolvePendingContent()}\n          </StateLayer>\n          <StateLayer\n            active={currentState === 'success'}\n            reducedMotion={prefersReducedMotion}\n            animateEnabled={animateEnabled}\n          >\n            {resolveSuccessContent()}\n          </StateLayer>\n          <StateLayer\n            active={currentState === 'error'}\n            reducedMotion={prefersReducedMotion}\n            animateEnabled={animateEnabled}\n          >\n            {resolveErrorContent()}\n          </StateLayer>\n        </Button>\n        <span className=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n          {announcements?.[currentState] ?? ''}\n        </span>\n      </>\n    )\n  },\n)\n\nActionButton.displayName = 'ActionButton'\n\nexport function ActionButtonPreview() {\n  const [demoState, setDemoState] = React.useState<ActionButtonState>('idle')\n\n  const simulateAction = React.useCallback(async (signal: AbortSignal) => {\n    await new Promise<void>((resolve, reject) => {\n      const timer = setTimeout(resolve, 1500)\n      signal.addEventListener('abort', () => {\n        clearTimeout(timer)\n        reject(new DOMException('Aborted', 'AbortError'))\n      })\n    })\n  }, [])\n\n  return (\n    <div className=\"flex flex-col items-center justify-center gap-6 p-6\">\n      <div className=\"flex flex-wrap items-center justify-center gap-3\">\n        <ActionButton\n          onAction={simulateAction}\n          idleLabel=\"Save\"\n          pendingLabel=\"Saving…\"\n          successLabel=\"Saved\"\n          errorLabel=\"Error\"\n          minPendingMs={600}\n          announcements={{\n            pending: 'Saving…',\n            success: 'Saved successfully',\n            error: 'Save failed',\n          }}\n        />\n        <ActionButton\n          variant=\"outline\"\n          onAction={async () => {\n            await new Promise((_, rej) => setTimeout(() => rej(new Error('fail')), 1200))\n          }}\n          idleLabel=\"Submit\"\n          pendingLabel=\"Submitting…\"\n          errorLabel=\"Retry\"\n          minPendingMs={400}\n        />\n      </div>\n\n      <div className=\"flex flex-wrap items-center justify-center gap-2\">\n        {(['idle', 'pending', 'success', 'error'] as const).map((s) => (\n          <button\n            key={s}\n            type=\"button\"\n            onClick={() => setDemoState(s)}\n            className={cn(\n              'rounded-md border px-2 py-1 text-xs capitalize',\n              demoState === s\n                ? 'border-(--color-accent) text-(--color-accent)'\n                : 'border-(--color-border) text-(--color-muted)',\n            )}\n          >\n            {s}\n          </button>\n        ))}\n      </div>\n      <ActionButton state={demoState} idleLabel=\"Controlled\" variant=\"outline\" />\n    </div>\n  )\n}\n"
    },
    {
      "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"
}
