{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-midi-playback",
  "type": "registry:ui",
  "title": "MIDI Playback",
  "description": "Shared MIDI playback panel extracted from Code Sequencer: timeline, stable colored tracks, host mute/solo callbacks, fit or following scroll view, export and drag actions. The host owns scheduling and MIDI file encoding.",
  "dependencies": [
    "@base-ui/react",
    "@webgpu/types",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-console-button.json",
    "https://pluginui.com/r/core-timeline-roll.json"
  ],
  "files": [
    {
      "path": "components/ui/midi-playback.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/midi-playback\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 */\nimport {\n  memo,\n  useState,\n  type CSSProperties,\n  type DragEvent,\n  type ReactNode,\n} from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport type { PlaybackClock } from \"maruaudio/ui/shared/playback-clock\"\nimport {\n  ConsoleIconButton,\n  ConsoleToggle,\n} from \"@/components/ui/console-button\"\nimport {\n  TimelineRoll,\n  type TimelineRollView,\n  type TimelineRollProps,\n} from \"@/components/ui/timeline-roll\"\nimport {\n  TimelineRollLegend,\n  type TimelineRollLegendProps,\n} from \"@/components/ui/timeline-roll-legend\"\nimport type {\n  SectionBand,\n  TimelineRollNote,\n  TimelineRollTrack,\n} from \"maruaudio/ui/shared/timeline-roll-math\"\n\nexport const MIDI_PLAYBACK_SCROLL_MIN_MEASURES = 20\n\n/** Display projection only. The producer owns its source model and MIDI encoding. */\nexport interface MidiPlaybackData {\n  readonly notes: readonly TimelineRollNote[]\n  readonly tracks: readonly TimelineRollTrack[]\n  readonly sections?: readonly SectionBand[]\n}\n\nexport interface MidiPlaybackToolbarProps {\n  /** Optional author-supplied export hover text. */\n  readonly exportTooltip?: ReactNode\n  /** Optional author-supplied scrolling hover text. */\n  readonly scrollTooltip?: ReactNode\n  /** A host's dock collapse button, tabs or transport controls. */\n  readonly leading?: ReactNode\n  /** Secondary producer actions, placed after export on the right. */\n  readonly trailing?: ReactNode\n  readonly title?: string\n  readonly summary?: string\n  readonly legend?: ReactNode\n  /** Hide scrolling controls below 20 measures. */\n  readonly cycles?: number\n  readonly view?: TimelineRollView\n  readonly onViewChange?: (view: TimelineRollView) => void\n  readonly exportLabel?: string\n  readonly onExport?: () => void\n  readonly onExportDrag?: (event: DragEvent<HTMLButtonElement>) => void\n  readonly onExportDragEnd?: () => void\n  readonly disabled?: boolean\n  readonly className?: string\n}\n\n/** The same actions can sit in a dock header or above a standalone roll. */\nexport const MidiPlaybackToolbar = memo(function MidiPlaybackToolbar(\n  props: MidiPlaybackToolbarProps\n) {\n  return (\n    <div\n      data-uikit-midi-playback-toolbar=\"\"\n      className={props.className}\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        flexWrap: \"wrap\",\n        gap: 6,\n        minWidth: 0,\n        minHeight: 32,\n        color: token.text,\n        fontFamily: token.font,\n      }}\n    >\n      {props.leading}\n      {props.title ? (\n        <span\n          aria-description={props.summary}\n          style={{\n            color: token.textMuted,\n            fontSize: 11,\n            fontWeight: 700,\n            letterSpacing: \"0.08em\",\n          }}\n        >\n          {props.title}\n        </span>\n      ) : null}\n      {/* The legend uses inline-size containment, so its intrinsic width is zero.\n          Reserve a useful basis before wrapping instead of squeezing its track\n          buttons into the few pixels left after the leading controls. */}\n      <div data-midi-playback-legend=\"\" style={{ flex: props.legend ? \"1 1 12rem\" : \"1 1 auto\", minWidth: 0 }}>{props.legend}</div>\n      {(props.cycles ?? Infinity) >= MIDI_PLAYBACK_SCROLL_MIN_MEASURES &&\n      props.view &&\n      props.onViewChange ? (\n        <ConsoleToggle\n          size=\"sm\"\n          icon=\"panel-right\"\n          pressed={props.view === \"detail\"}\n          disabled={props.disabled}\n          onPressedChange={(detail) =>\n            props.onViewChange?.(detail ? \"detail\" : \"fit\")\n          }\n          label=\"Scroll along the pattern\"\n          tooltip={props.scrollTooltip}\n\n        />\n      ) : null}\n      {props.onExport ? (\n        <ConsoleIconButton\n          size=\"sm\"\n          icon=\"download\"\n          label={props.exportLabel ?? \"Export MIDI\"}\n          tooltip={props.exportTooltip}\n          disabled={props.disabled}\n          draggable={!props.disabled && Boolean(props.onExportDrag)}\n          onClick={props.onExport}\n          onDragStart={props.onExportDrag}\n          onDragEnd={props.onExportDragEnd}\n        />\n      ) : null}\n      {props.trailing}\n    </div>\n  )\n})\n\nexport interface MidiPlaybackDisplayProps extends Pick<\n  TimelineRollProps,\n  \"leadingLane\" | \"restingPositionSeconds\" | \"onSeek\" | \"plotActivation\" | \"spectrogram\" | \"noteOpacity\" | \"rulerUnit\" | \"highlightedDrumMidi\" | \"onDrumHoverChange\"\n> {\n  readonly data: MidiPlaybackData | null\n  readonly durationSeconds: number\n  readonly cycles: number\n  readonly view: TimelineRollView\n  readonly clock: PlaybackClock\n  readonly follow?: boolean\n  readonly soloed?: readonly string[]\n  readonly legend?: ReactNode\n  readonly emptyMessage?: string\n  readonly className?: string\n}\n\n/** No scheduler or synthesizer: only the playhead subscribes to the host clock. */\nexport const MidiPlaybackDisplay = memo(function MidiPlaybackDisplay(\n  props: MidiPlaybackDisplayProps\n) {\n  if (!props.data)\n    return (\n      <p\n        style={{ margin: 0, padding: 4, color: token.textMuted, fontSize: 12 }}\n      >\n        {props.emptyMessage ?? \"Preview to draw the notes.\"}\n      </p>\n    )\n  return (\n    <div\n      data-uikit-midi-playback-display=\"\"\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        flex: \"1 1 auto\",\n        minWidth: 0,\n        minHeight: 0,\n        height: \"100%\",\n      }}\n    >\n      {props.legend}\n      <TimelineRoll\n        highlightedDrumMidi={props.highlightedDrumMidi}\n        onDrumHoverChange={props.onDrumHoverChange}\n        onSeek={props.onSeek}\n        plotActivation={props.plotActivation}\n        spectrogram={props.spectrogram}\n        noteOpacity={props.noteOpacity}\n        leadingLane={props.leadingLane}\n        restingPositionSeconds={props.restingPositionSeconds}\n        notes={props.data.notes}\n        tracks={props.data.tracks}\n        sections={props.data.sections}\n        durationSeconds={props.durationSeconds}\n        cycles={props.cycles}\n        rulerUnit={props.rulerUnit}\n        view={props.view}\n        clock={props.clock}\n        follow={props.follow}\n        soloed={props.soloed}\n        className={props.className}\n      />\n    </div>\n  )\n})\n\nexport interface MidiPlaybackProps\n  extends\n    Omit<MidiPlaybackDisplayProps, \"view\" | \"legend\">,\n    Pick<\n      TimelineRollLegendProps,\n      \"onToggleMute\" | \"onToggleSolo\" | \"showNoteCounts\"\n    >,\n    Pick<\n      MidiPlaybackToolbarProps,\n      | \"onExport\"\n      | \"onExportDrag\"\n      | \"onExportDragEnd\"\n      | \"exportLabel\"\n      | \"exportTooltip\"\n      | \"scrollTooltip\"\n      | \"trailing\"\n    > {\n  readonly controls?: ReactNode\n  readonly view?: TimelineRollView\n  readonly onViewChange?: (view: TimelineRollView) => void\n  readonly height?: number | string\n  readonly style?: CSSProperties\n}\n\n/** Complete controlled MIDI display, track legend, scrolling and export affordance. */\nexport function MidiPlayback(props: MidiPlaybackProps) {\n  const [localView, setLocalView] = useState<TimelineRollView>(\"fit\")\n  const view =\n    props.cycles < MIDI_PLAYBACK_SCROLL_MIN_MEASURES\n      ? \"fit\"\n      : (props.view ?? localView)\n  return (\n    <section\n      data-uikit-midi-playback=\"\"\n      aria-label=\"MIDI playback\"\n      className={props.className}\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 4,\n        minWidth: 0,\n        color: token.text,\n        ...props.style,\n      }}\n    >\n      <MidiPlaybackToolbar\n        leading={props.controls}\n        trailing={props.trailing}\n        legend={\n          props.data ? (\n            <TimelineRollLegend\n              showNoteCounts={props.showNoteCounts}\n              tracks={props.data.tracks}\n              soloed={props.soloed}\n              onToggleMute={props.onToggleMute}\n              onToggleSolo={props.onToggleSolo}\n            />\n          ) : undefined\n        }\n        cycles={props.cycles}\n        view={view}\n        onViewChange={props.onViewChange ?? setLocalView}\n        disabled={!props.data?.notes.length}\n        onExport={props.onExport}\n        onExportDrag={props.onExportDrag}\n        onExportDragEnd={props.onExportDragEnd}\n        exportLabel={props.exportLabel}\n        exportTooltip={props.exportTooltip}\n        scrollTooltip={props.scrollTooltip}\n      />\n      <div\n        style={{\n          height: props.height ?? 220,\n          minHeight: 72,\n          display: \"flex\",\n          flexDirection: \"column\",\n          minWidth: 0,\n        }}\n      >\n        <MidiPlaybackDisplay {...props} className={undefined} view={view} />\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/midi-playback.tsx"
    }
  ]
}
