{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-midi-activity-table",
  "type": "registry:ui",
  "title": "MIDI Activity Table",
  "description": "A coalesced live view of MIDI controls — one row each, with an inline SVG sparkline of recent values, a level bar, held state and a message count. Built for diagnosing a controller without drowning in event spam.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/midi-activity-table.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/midi-activity-table\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 * A *coalesced* MIDI activity view: one row per control, not per message.\n *\n * The problem this solves is event spam. A single filter sweep emits hundreds\n * of CC messages; a flat log turns into a blur and you lose the thing you were\n * actually watching. So a CC gets one row that updates in place, carrying a\n * sparkline of its recent values, and a note gets one row that shows held/\n * released rather than two entries.\n *\n * The sparkline is inline SVG on purpose — ported from the midimama monitor,\n * which drew it the same way. A charting library for a 200×20 polyline would\n * be a dependency for nothing, and these redraw at MIDI rate.\n *\n * Controlled and presentational: the caller owns the coalescing state (see\n * `useMidiActivity`) and this renders it.\n */\n\nimport { useMemo } from \"react\"\n\nexport interface MidiActivityRow {\n  /** Stable identity: `${source}:${channel}:${kind}:${number}`. */\n  key: string\n  source?: string\n  channel: number\n  /** `cc`, `note`, `pitchBend`, `channelPressure`, … */\n  kind: string\n  /** CC number or note number; absent for channel-wide messages. */\n  number?: number\n  /** Display name — `CC 74`, `C4`, `Pitch bend`. */\n  label: string\n  /** Latest value, 0..127 (or 0..16383 for pitch bend). */\n  value: number\n  /** Max value for scaling the sparkline and bar. */\n  max: number\n  /** True while a note is held or a button is down. */\n  held: boolean\n  /** Recent values, oldest first. */\n  history: readonly number[]\n  /** Messages seen since the row appeared. */\n  count: number\n  /** performance.now() of the most recent message. */\n  updatedAt: number\n  /** Resolved semantic function, when the caller knows one. */\n  resolved?: string | null\n}\n\nexport interface MidiActivityTableProps {\n  rows: readonly MidiActivityRow[]\n  showSource?: boolean\n  /** Hide unavailable channel/event history in host note-snapshot views. */\n  showChannel?: boolean\n  showRecent?: boolean\n  showCount?: boolean\n  controlLabel?: string\n  valueLabel?: string\n  /** Format unavailable or domain-specific values without inventing measurements. */\n  renderValue?: (row: MidiActivityRow) => React.ReactNode\n  /** Compact consumers may use 0; event monitors retain the 44rem default. */\n  minWidth?: number | string\n  /** Column header for the resolved-function column; omit to hide it. */\n  resolvedLabel?: string\n  empty?: React.ReactNode\n  className?: string\n}\n\n/** Sparkline path over `history`, scaled to `max`. */\nfunction sparkPath(\n  history: readonly number[],\n  max: number,\n  w: number,\n  h: number\n): string {\n  if (history.length < 2) return \"\"\n  const pad = 1.5\n  const span = history.length - 1\n  return history\n    .map((v, i) => {\n      const x = pad + (i / span) * (w - 2 * pad)\n      const y = h - pad - (Math.min(v, max) / max) * (h - 2 * pad)\n      return `${i === 0 ? \"M\" : \"L\"} ${x.toFixed(1)},${y.toFixed(1)}`\n    })\n    .join(\" \")\n}\n\nconst KIND_TONE: Record<string, string> = {\n  cc: \"text-sky-600 dark:text-sky-400\",\n  note: \"text-emerald-600 dark:text-emerald-400\",\n  pitchBend: \"text-amber-600 dark:text-amber-400\",\n  channelPressure: \"text-orange-600 dark:text-orange-400\",\n  polyPressure: \"text-purple-600 dark:text-purple-400\",\n  programChange: \"text-pink-600 dark:text-pink-400\",\n}\n\nexport function MidiActivityTable({\n  rows,\n  showSource = false,\n  showChannel = true,\n  showRecent = true,\n  showCount = true,\n  controlLabel = \"Control\",\n  valueLabel = \"Value\",\n  renderValue,\n  minWidth = \"44rem\",\n  resolvedLabel,\n  empty = \"Waiting for MIDI…\",\n  className = \"\",\n}: MidiActivityTableProps) {\n  const now = useMemo(\n    () => (rows.length > 0 ? Math.max(...rows.map((r) => r.updatedAt)) : 0),\n    [rows]\n  )\n  const cols =\n    2 +\n    Number(showChannel) +\n    Number(showRecent) +\n    Number(showCount) +\n    Number(showSource) +\n    Number(Boolean(resolvedLabel))\n\n  return (\n    <div\n      className={`overflow-x-auto rounded-xl border border-current/10 ${className}`}\n    >\n      <table className=\"w-full text-left text-sm\" style={{ minWidth }}>\n        <thead className=\"border-b border-current/10 text-xs tracking-wide uppercase opacity-60\">\n          <tr>\n            {showSource && <th className=\"px-3 py-2\">Source</th>}\n            <th className=\"px-3 py-2\">{controlLabel}</th>\n            {showChannel && <th className=\"px-3 py-2\">Ch</th>}\n            {resolvedLabel && <th className=\"px-3 py-2\">{resolvedLabel}</th>}\n            <th className=\"px-3 py-2 text-right\">{valueLabel}</th>\n            {showRecent && <th className=\"px-3 py-2\">Recent</th>}\n            {showCount && <th className=\"px-3 py-2 text-right\">Count</th>}\n          </tr>\n        </thead>\n        <tbody>\n          {rows.length === 0 && (\n            <tr>\n              <td colSpan={cols} className=\"px-3 py-8 text-center opacity-50\">\n                {empty}\n              </td>\n            </tr>\n          )}\n          {rows.map((row) => {\n            // Fade rows that have gone quiet, so the eye tracks what is moving.\n            const stale = now - row.updatedAt > 1500\n            return (\n              <tr\n                key={row.key}\n                className={`border-b border-current/5 transition-opacity ${\n                  stale && !row.held ? \"opacity-45\" : \"\"\n                }`}\n              >\n                {showSource && (\n                  <td className=\"max-w-28 truncate px-3 py-1.5 text-xs opacity-60\">\n                    {row.source ?? \"—\"}\n                  </td>\n                )}\n                <td className=\"px-3 py-1.5\">\n                  <span className=\"flex items-center gap-2\">\n                    <span\n                      aria-hidden\n                      className={`size-1.5 shrink-0 rounded-full ${\n                        row.held ? \"bg-emerald-500\" : \"bg-current/25\"\n                      }`}\n                    />\n                    <span\n                      className={`font-mono text-xs font-medium ${KIND_TONE[row.kind] ?? \"\"}`}\n                    >\n                      {row.label}\n                    </span>\n                  </span>\n                </td>\n                {showChannel && (\n                  <td className=\"px-3 py-1.5 text-xs tabular-nums opacity-60\">\n                    {row.channel}\n                  </td>\n                )}\n                {resolvedLabel && (\n                  <td className=\"px-3 py-1.5 font-mono text-xs font-semibold\">\n                    {row.resolved ?? (\n                      <span className=\"font-normal opacity-40\">—</span>\n                    )}\n                  </td>\n                )}\n                <td className=\"px-3 py-1.5 text-right text-xs tabular-nums\">\n                  {renderValue ? renderValue(row) : row.value}\n                </td>\n                {showRecent && (\n                  <td className=\"px-3 py-1.5\">\n                    <span className=\"flex items-center gap-2\">\n                      <svg\n                        width={120}\n                        height={20}\n                        viewBox=\"0 0 120 20\"\n                        className=\"shrink-0 overflow-visible\"\n                        aria-hidden\n                      >\n                        <path\n                          d={sparkPath(row.history, row.max, 120, 20)}\n                          fill=\"none\"\n                          stroke=\"currentColor\"\n                          strokeWidth={1.25}\n                          className=\"opacity-70\"\n                        />\n                      </svg>\n                      <span className=\"h-1 w-10 shrink-0 overflow-hidden rounded-full bg-current/15\">\n                        <span\n                          className=\"block h-full rounded-full bg-current/60\"\n                          style={{\n                            width: `${Math.round((row.value / row.max) * 100)}%`,\n                          }}\n                        />\n                      </span>\n                    </span>\n                  </td>\n                )}\n                {showCount && (\n                  <td className=\"px-3 py-1.5 text-right text-xs tabular-nums opacity-50\">\n                    {row.count}\n                  </td>\n                )}\n              </tr>\n            )\n          })}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/midi-activity-table.tsx"
    }
  ]
}
