{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-console-button",
  "type": "registry:ui",
  "title": "Console Button",
  "description": "The plain buttons of a console face on Base UI's Button and Toggle: ConsoleButton (text with an optional console icon), ConsoleIconButton (a square icon-only button named by its label, which is also its tooltip) and ConsoleToggle (aria-pressed on/off with an accent border when pressed). Three heights read from the face's --uikit-control-h-sm|md|lg variables (24, 28, 32px by default, 44px on a touch face), four variants (ghost, outline, primary, subtle), an optional ConsoleTooltip with a shortcut, and focusableWhenDisabled so a busy or refused button keeps focus with aria-disabled. Hover, focus and pressed colours come from its own hoisted stylesheet, with transitions only under prefers-reduced-motion: no-preference.",
  "dependencies": [
    "@base-ui/react",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-console-tooltip.json"
  ],
  "files": [
    {
      "path": "components/ui/console-button.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/console-button\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\n/**\n * ConsoleButton, ConsoleIconButton and ConsoleToggle — the plain buttons of a\n * console face: \"Render\", the play and export icons in a header, a Loop or\n * Follow toggle.\n *\n * Built on Base UI's Button and Toggle, which own the button semantics, the\n * disabled state and, for the toggle, `aria-pressed`. What this adds is one\n * look for every console button: three heights read from the face's control\n * height variables (`--uikit-control-h-sm|md|lg`, 24, 28 and 32px unless the\n * face sets them, for instance to 44px on a touch face), four variants, an\n * optional icon from the console icon set and an optional tooltip.\n *\n *   <ConsoleButton variant=\"primary\" icon=\"play\" shortcut=\"⌘↵\">Preview</ConsoleButton>\n *   <ConsoleIconButton icon=\"download\" label=\"Export MIDI\" />\n *   <ConsoleToggle icon=\"loop\" label=\"Loop\" pressed={loop} onPressedChange={setLoop} />\n *\n * A busy or refused button can stay focusable: `disabled` with\n * `focusableWhenDisabled` renders `aria-disabled=\"true\"` instead of the\n * native `disabled` attribute, so focus does not fall to the page when a\n * button goes busy under the keyboard, and its tooltip still opens on hover\n * and focus (a natively disabled button fires neither).\n *\n * Hover, focus and pressed colours live in a small stylesheet that React 19\n * hoists into the head once per document (`ConsoleButtonStyles`); sizes and\n * layout are inline. Every transition sits behind\n * `prefers-reduced-motion: no-preference`.\n *\n * Contrast with the default token fallbacks: text 15:1 (outline, subtle),\n * muted ghost text 5.3:1 on the surface, primary text 4.8:1 on its fill; the\n * pressed toggle's accent border 4:1 against the surface. A face with a light\n * accent sets `--uikit-accent-contrast` to a dark ink.\n */\n\nimport {\n  createElement,\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  type JSX,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n} from \"react\"\nimport { Button } from \"@base-ui/react/button\"\nimport { Toggle } from \"@base-ui/react/toggle\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  ConsoleIcon,\n  type ConsoleIconName,\n} from \"maruaudio/ui/shared/console-icons\"\nimport { ConsoleTooltip } from \"@/components/ui/console-tooltip\"\n\n/** The three console control heights, in pixels, before a face overrides them. */\nexport const CONSOLE_CONTROL_HEIGHTS = { sm: 24, md: 28, lg: 32 } as const\n\n/** A console control size: 24, 28 or 32px tall by default. */\nexport type ConsoleControlSize = keyof typeof CONSOLE_CONTROL_HEIGHTS\n\n/**\n * The CSS height of a console control: the face's control-height variable,\n * falling back to the default pixel height.\n */\nexport function consoleControlHeight(size: ConsoleControlSize): string {\n  return `var(--uikit-control-h-${size}, ${CONSOLE_CONTROL_HEIGHTS[size]}px)`\n}\n\nexport type ConsoleButtonVariant = \"ghost\" | \"outline\" | \"primary\" | \"subtle\"\n\n/** Text size of console buttons, in pixels. */\nconst FONT_SIZE = 12\n\nconst TEXT = \"var(--uikit-text, #e8eaf0)\"\nconst MUTED = \"var(--uikit-text-muted, rgba(232, 234, 240, 0.55))\"\nconst HOVER = \"color-mix(in srgb, var(--uikit-text, #e8eaf0) 9%, transparent)\"\nconst FOCUS = \"var(--uikit-focus, var(--uikit-accent, #6366f1))\"\nconst BORDER = \"var(--uikit-border, rgba(148, 163, 184, 0.35))\"\nconst CAP = \"var(--uikit-cap, #262b36)\"\n/*\n * The primary fill. Its fallback is a shade deeper than the kit's default\n * accent (#6366f1) because white text on #6366f1 measures 4.47:1, just under\n * AA; #5d61ee measures 4.76:1. A face that sets `--uikit-accent` gets its own.\n */\nconst PRIMARY = \"var(--uikit-accent, #5d61ee)\"\nconst ON_PRIMARY = \"var(--uikit-accent-contrast, #ffffff)\"\nconst ACCENT = \"var(--uikit-accent, #6366f1)\"\nconst PRESSED_WASH =\n  \"color-mix(in srgb, var(--uikit-accent, #6366f1) 20%, transparent)\"\n\n/**\n * The console button stylesheet, one line. Rendered through\n * {@link ConsoleButtonStyles}; exported for hosts that inject styles\n * themselves.\n */\nexport const CONSOLE_BUTTON_STYLES = String.raw`\n[data-console-button]{background:transparent;color:${MUTED};border-color:transparent;font-weight:500}\n[data-console-button][data-variant=\"outline\"]{color:${TEXT};border-color:${BORDER}}\n[data-console-button][data-variant=\"subtle\"]{color:${TEXT};background:${CAP}}\n[data-console-button][data-variant=\"primary\"]{color:${ON_PRIMARY};background:${PRIMARY};border-color:${PRIMARY};font-weight:600}\n[data-console-button][data-pressed]{color:${TEXT};background:${PRESSED_WASH};border-color:${ACCENT}}\n@media (hover: hover){\n[data-console-button]:hover:not([data-disabled]){color:${TEXT};background:${HOVER}}\n[data-console-button][data-variant=\"subtle\"]:hover:not([data-disabled]){background:color-mix(in srgb, ${TEXT} 9%, ${CAP})}\n[data-console-button][data-variant=\"primary\"]:hover:not([data-disabled]){color:${ON_PRIMARY};background:${PRIMARY};filter:brightness(1.1)}\n[data-console-button][data-pressed]:hover:not([data-disabled]){background:color-mix(in srgb, ${ACCENT} 28%, transparent)}\n}\n[data-console-button]:active:not([data-disabled]){filter:brightness(.92)}\n[data-console-button][data-disabled]{opacity:.45;cursor:not-allowed}\n[data-console-button]:focus-visible{outline:2px solid ${FOCUS};outline-offset:1px}\n[data-console-button] kbd{font:inherit;opacity:.8}\n@media (prefers-reduced-motion: no-preference){\n[data-console-button]{transition:background-color 120ms ease-out,color 120ms ease-out,border-color 120ms ease-out,filter 120ms ease-out}\n}\n`.replace(/\\n/g, \"\")\n\n/** The `href` React dedupes the console button stylesheet by. */\nexport const CONSOLE_BUTTON_STYLES_HREF = \"uikit-console-button\"\n\n/**\n * The console button stylesheet as a hoisted `<style>`: React 19 moves it into\n * the document head and keeps one copy per document, on the server and in the\n * browser. Render it beside any element that carries `data-console-button`.\n */\nexport function ConsoleButtonStyles(): ReactElement {\n  return createElement(\n    \"style\",\n    { href: CONSOLE_BUTTON_STYLES_HREF, precedence: \"default\" },\n    CONSOLE_BUTTON_STYLES\n  )\n}\n\n/** The size of a glyph inside a button of the given size, in pixels. */\nexport function consoleIconSize(size: ConsoleControlSize): number {\n  return size === \"lg\" ? 16 : 14\n}\n\n/**\n * The inline layout of a console button: the control height, a square when\n * `square` (width equals height), otherwise a padded pill.\n */\nexport function consoleButtonStyle(\n  size: ConsoleControlSize,\n  square: boolean\n): CSSProperties {\n  const height = consoleControlHeight(size)\n  return {\n    boxSizing: \"border-box\",\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    gap: 6,\n    flex: \"none\",\n    height,\n    minWidth: height,\n    width: square ? height : undefined,\n    padding: square ? 0 : size === \"sm\" ? \"0 8px\" : \"0 10px\",\n    margin: 0,\n    // Width and style only: the colour belongs to the stylesheet's states.\n    borderWidth: 1,\n    borderStyle: \"solid\",\n    borderRadius: 6,\n    fontFamily: token.font,\n    fontSize: FONT_SIZE,\n    lineHeight: 1,\n    whiteSpace: \"nowrap\",\n    cursor: \"pointer\",\n    userSelect: \"none\",\n    WebkitTapHighlightColor: \"transparent\",\n  }\n}\n\ntype NativeButtonProps = Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  \"disabled\" | \"children\" | \"style\" | \"className\" | \"type\"\n>\n\nexport interface ConsoleButtonProps extends NativeButtonProps {\n  /** Control height: 24, 28 or 32px unless the face sets the variables. Defaults to \"md\". */\n  readonly size?: ConsoleControlSize\n  /** Defaults to \"outline\". \"primary\" is the one main action of a panel. */\n  readonly variant?: ConsoleButtonVariant\n  /** A console glyph before the text. */\n  readonly icon?: ConsoleIconName\n  /** A tooltip on hover and keyboard focus. Visual only: the button's own text or `aria-label` names it. */\n  readonly tooltip?: ReactNode\n  /** A keyboard shortcut shown in the tooltip, such as \"⌘↵\". Pair with `aria-keyshortcuts`. */\n  readonly shortcut?: string\n  readonly disabled?: boolean\n  /**\n   * With `disabled`: stay focusable and hoverable, with `aria-disabled` in\n   * place of the native attribute. Use it for a busy or refused button that\n   * may hold focus.\n   */\n  readonly focusableWhenDisabled?: boolean\n  readonly type?: \"button\" | \"submit\" | \"reset\"\n  readonly children?: ReactNode\n  readonly className?: string\n  readonly style?: CSSProperties\n  readonly ref?: Ref<HTMLButtonElement>\n}\n\n/** Wrap `element` in a tooltip when there is one. */\nfunction withTooltip(\n  element: ReactElement,\n  tooltip: ReactNode,\n  shortcut: string | undefined\n): ReactElement {\n  if (tooltip === undefined || tooltip === null || tooltip === false) {\n    return element\n  }\n  return (\n    <ConsoleTooltip content={tooltip} shortcut={shortcut}>\n      {element}\n    </ConsoleTooltip>\n  )\n}\n\n/** The shared body of ConsoleButton and ConsoleIconButton. */\nfunction renderConsoleButton(\n  {\n    size = \"md\",\n    variant = \"outline\",\n    icon,\n    tooltip,\n    shortcut,\n    disabled = false,\n    focusableWhenDisabled = false,\n    type = \"button\",\n    children,\n    className,\n    style,\n    ref,\n    ...props\n  }: ConsoleButtonProps,\n  square: boolean\n): ReactElement {\n  const button = (\n    <Button\n      {...props}\n      ref={ref}\n      type={type}\n      disabled={disabled}\n      // Only while disabled: Base UI would otherwise write aria-disabled=\"false\".\n      focusableWhenDisabled={disabled && focusableWhenDisabled}\n      className={className}\n      data-console-button=\"\"\n      data-variant={variant}\n      data-size={size}\n      data-icon-only={square ? \"\" : undefined}\n      style={{ ...consoleButtonStyle(size, square), ...style }}\n    >\n      <ConsoleButtonStyles />\n      {icon ? <ConsoleIcon name={icon} size={consoleIconSize(size)} /> : null}\n      {children}\n    </Button>\n  )\n  return withTooltip(button, tooltip, shortcut)\n}\n\n/** A console button with text, and optionally an icon before it. */\nexport function ConsoleButton(props: ConsoleButtonProps): JSX.Element {\n  const { children } = props\n  const hasText =\n    children !== undefined && children !== null && children !== false\n  return renderConsoleButton(props, !hasText)\n}\n\nexport interface ConsoleIconButtonProps extends Omit<\n  ConsoleButtonProps,\n  \"icon\" | \"tooltip\" | \"aria-label\"\n> {\n  /** The glyph. Pass `children` instead for a custom one. */\n  readonly icon?: ConsoleIconName\n  /** The accessible name. Hover text is supplied separately with `tooltip`. */\n  readonly label: string\n  /** Optional hover text; omitted or `false` shows no tooltip. */\n  readonly tooltip?: ReactNode | false\n}\n\n/**\n * A square icon-only console button, as wide as it is tall. `label` is its\n * accessible name; hover text is opt-in.\n */\nexport function ConsoleIconButton({\n  icon,\n  label,\n  tooltip,\n  size = \"md\",\n  variant = \"ghost\",\n  children,\n  ...props\n}: ConsoleIconButtonProps): JSX.Element {\n  return renderConsoleButton(\n    {\n      ...props,\n      size,\n      variant,\n      \"aria-label\": label,\n      tooltip: tooltip === false ? undefined : tooltip,\n      children:\n        children ??\n        (icon ? (\n          <ConsoleIcon name={icon} size={consoleIconSize(size)} />\n        ) : null),\n    },\n    true\n  )\n}\n\nexport interface ConsoleToggleProps {\n  readonly pressed: boolean\n  readonly onPressedChange: (pressed: boolean) => void\n  /**\n   * The toggle's name. Shown as its text when there is no icon and no\n   * `children`; with an icon alone it is the `aria-label`.\n   */\n  readonly label: string\n  readonly icon?: ConsoleIconName\n  /** Visible content after the icon; the label is shown when omitted and there is no icon. */\n  readonly children?: ReactNode\n  /** Optional hover text; omitted or `false` shows no tooltip. */\n  readonly tooltip?: ReactNode | false\n  readonly shortcut?: string\n  /** Defaults to \"md\". */\n  readonly size?: ConsoleControlSize\n  /** \"ghost\" (default) or \"outline\" when not pressed; pressed always shows the accent border. */\n  readonly variant?: \"ghost\" | \"outline\"\n  readonly disabled?: boolean\n  readonly id?: string\n  readonly \"aria-controls\"?: string\n  readonly \"aria-keyshortcuts\"?: string\n  readonly className?: string\n  readonly style?: CSSProperties\n  readonly ref?: Ref<HTMLButtonElement>\n}\n\n/**\n * A console on/off button (`aria-pressed`) on Base UI's Toggle: Loop, Follow,\n * Mute. Pressed draws an accent border over a light accent wash.\n */\nexport function ConsoleToggle({\n  pressed,\n  onPressedChange,\n  label,\n  icon,\n  children,\n  tooltip,\n  shortcut,\n  size = \"md\",\n  variant = \"ghost\",\n  disabled = false,\n  id,\n  \"aria-controls\": ariaControls,\n  \"aria-keyshortcuts\": ariaKeyShortcuts,\n  className,\n  style,\n  ref,\n}: ConsoleToggleProps): JSX.Element {\n  const text = children ?? (icon ? null : label)\n  const iconOnly = text === null || text === undefined\n  const toggle = (\n    <Toggle\n      ref={ref}\n      id={id}\n      pressed={pressed}\n      onPressedChange={(next) => onPressedChange(next)}\n      disabled={disabled}\n      aria-label={iconOnly ? label : undefined}\n      aria-controls={ariaControls}\n      aria-keyshortcuts={ariaKeyShortcuts}\n      className={className}\n      data-console-button=\"\"\n      data-console-toggle=\"\"\n      data-variant={variant}\n      data-size={size}\n      style={{ ...consoleButtonStyle(size, iconOnly), ...style }}\n    >\n      <ConsoleButtonStyles />\n      {icon ? <ConsoleIcon name={icon} size={consoleIconSize(size)} /> : null}\n      {text}\n    </Toggle>\n  )\n  const tip =\n    tooltip === false ? undefined : tooltip\n  return withTooltip(toggle, tip, shortcut)\n}\n",
      "type": "registry:component",
      "target": "@ui/console-button.tsx"
    }
  ]
}
