import * as React from 'react'; import { panelButtonChrome, useKnobTheme } from '../core/theme'; export type TransportKind = | 'play' | 'pause' | 'stop' | 'record' | 'rewind' | 'forward' | 'panic'; export interface TransportButtonProps { kind: TransportKind; /** Engaged/lit state (playing, armed, recording). Reflects app state. */ active?: boolean; onClick?: () => void; /** Pulse the glyph while active (auto-on for `record`). Reduced-motion aware. */ blink?: boolean; /** Override the glyph/LED color. Defaults per kind (play green, record red…). */ color?: string; /** Button size in px (square). Default: 38. */ size?: number; disabled?: boolean; className?: string; style?: React.CSSProperties; 'aria-label'?: string; } const LABEL: Record = { play: 'Play', pause: 'Pause', stop: 'Stop', record: 'Record', rewind: 'Rewind', forward: 'Forward', panic: 'Panic — all notes off', }; /** Glyph in a 24×24 box, filled with `currentColor`. */ const Glyph: React.FC<{ kind: TransportKind }> = ({ kind }) => { switch (kind) { case 'play': return ; case 'pause': return ( <> ); case 'stop': return ; case 'record': return ; case 'rewind': return ( <> ); case 'forward': return ( <> ); case 'panic': return ( <> ); } }; /** * A record / play / stop / pause / panic transport button, styled to sit * alongside dreamknob controls. `active` lights it; `record` blinks while * armed. Reduced-motion aware. */ export const TransportButton = React.forwardRef( function TransportButton( { kind, active = false, onClick, blink, color, size = 38, disabled = false, className, style, ...aria }, ref, ) { const theme = useKnobTheme(); const [focusVisible, setFocusVisible] = React.useState(false); const glyphRef = React.useRef(null); const chrome = panelButtonChrome(theme.scheme, active); const accent = color ?? (kind === 'record' || kind === 'panic' ? theme.zoneHot : kind === 'play' ? theme.zoneGood : kind === 'pause' ? theme.zoneWarn : theme.text); const lit = active ? accent : theme.label; const shouldBlink = (blink ?? kind === 'record') && active; React.useEffect(() => { const el = glyphRef.current; if (!el || !shouldBlink) return; if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return; const anim = el.animate([{ opacity: 1 }, { opacity: 0.3 }], { duration: 560, iterations: Infinity, direction: 'alternate', easing: 'ease-in-out', }); return () => anim.cancel(); }, [shouldBlink]); return ( ); }, );