{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-cc-mapping-list",
  "type": "registry:ui",
  "title": "CC Mapping List",
  "description": "Every MIDI CC mapping of a host as one grouped list: by module or rack instance, with a by-CC sort, search and filters. Rows show the CC, input and channel, target, range and live activity, with Learn, Pick control, enable and remove; opening a row gives CC, channel and range, and Advanced adds input, value mode, takeover, curve and invert. Import, export, reset and clear are callbacks; the list keeps only view state.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/cc-mapping-list.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/cc-mapping-list\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 * CcMappingList — every MIDI CC mapping of a host, as one grouped list.\n *\n * The full mappings surface's content (SC-AUDIO-291). A host wraps it in its\n * own modal (the website's \"MIDI mappings\" dialog); a native webview can wrap\n * it in whatever sheet it has. It is driven only by data and callbacks:\n *\n *   - the host's `CcMap`, its subjects (module or rack instances, each with\n *     its parameter schema), live activity by CC and input labels;\n *   - learn availability and its reason, and optional host capabilities;\n *   - callbacks for patch, remove, learn, pick-target, reset, clear, import\n *     and export.\n *\n * It keeps no state beyond view state (search, filter, sort, which row is\n * open). Every edit is a callback, so the host applies it through its one\n * mapping owner — the same function a badge edit or a control-menu edit uses.\n *\n * Information hierarchy: a row shows `CC n`, the input and channel (\"Any\n * input · omni\" when unset), the target with its path secondary, a range\n * summary and a live activity pulse. Opening a row shows the basic fields —\n * CC, channel, min/max — and **Advanced** reveals input, value mode,\n * takeover, curve and invert. Rows whose target or input is not available\n * stay visible and are marked unresolved (SC-AUDIO-275).\n *\n * Styling inherits the host's colours (`currentColor`, UI-kit tokens only for\n * borders and accent), so it reads correctly in a light site dialog and in a\n * dark plugin webview.\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport type { CcBinding, CcMap, ParamSchema } from \"maruaudio/ui/shared/maru/contracts\"\nimport {\n  analyzeCcMap,\n  ccBindingSourceKey,\n  ccBindingTargetKey,\n} from \"maruaudio/ui/shared/maru/contracts/cc\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport type { CcBindingPatch } from \"maruaudio/ui/shared/cc-dnd\"\nimport {\n  ccRoleName,\n  ccRoleShortLabel,\n} from \"maruaudio/ui/shared/cc-dnd\"\n\n/** A module or rack instance whose parameters the map can target. */\nexport interface CcMappingListSubject {\n  /** Rack/graph instance id; omitted for a single module's flat map. */\n  readonly id?: string\n  readonly label: string\n  readonly schema: ParamSchema\n}\n\n/**\n * What the host can honour. Omitted fields mean \"yes\" (the browser host);\n * a native host that cannot filter by input or shape values says so and the\n * list hides those fields rather than offering controls that do nothing.\n */\nexport interface CcMappingListCapabilities {\n  readonly sourceIdentity?: boolean\n  readonly shaping?: boolean\n  readonly learn?: boolean\n}\n\nexport type CcMappingListFilter = \"all\" | \"enabled\" | \"disabled\" | \"conflicts\"\nexport type CcMappingListSort = \"subject\" | \"cc\" | \"source\"\n\nexport interface CcMappingListProps {\n  map: CcMap\n  subjects: readonly CcMappingListSubject[]\n  /** Live 0–127 values keyed by CC. */\n  activity?: Readonly<Record<number, number>>\n  /** Input id → label, for the host's current inputs. */\n  inputLabels?: Readonly<Record<string, string>>\n  learnAvailable: boolean\n  learnUnavailableReason?: string | null\n  capabilities?: CcMappingListCapabilities\n  onPatch: (binding: CcBinding, patch: CcBindingPatch) => void\n  onRemove: (binding: CcBinding) => void\n  /** Arm a learn for this row (arms, never toggles). */\n  onLearn?: (binding: CcBinding) => void\n  /** Retarget: choose another control for this binding. */\n  onPickTarget?: (binding: CcBinding) => void\n  onResetDefaults?: () => void\n  onClearAll?: () => void\n  onImport?: (value: unknown) => void\n  /** Defaults to downloading `midi-mappings.json`. */\n  onExport?: (map: CcMap) => void\n  /** A row to open and focus on mount (`ccMappingRowKey`). */\n  focusKey?: string | null\n  /** Host actions placed in the toolbar (e.g. Add mapping). */\n  toolbar?: ReactNode\n}\n\n/** A stable key for one binding: target, input, channel and controller. */\nexport function ccMappingRowKey(\n  binding: Pick<CcBinding, \"subjectId\" | \"path\" | \"sourceId\" | \"channel\" | \"cc\">\n): string {\n  return [\n    binding.subjectId ?? \"\",\n    binding.path,\n    binding.sourceId ?? \"*\",\n    binding.channel ?? 0,\n    binding.cc,\n  ].join(\"\\u001f\")\n}\n\n/** The key of every row on a target (subject + path), for focusing. */\nexport function ccMappingTargetKey(\n  subjectId: string | undefined,\n  path: string\n): string {\n  return `${subjectId ?? \"\"}\\u001f${path}\\u001f`\n}\n\nexport interface CcMappingListTarget {\n  readonly subject: string\n  readonly label: string\n  readonly group?: string\n  readonly min: number\n  readonly max: number\n  readonly unit?: string\n}\n\nexport interface CcMappingListRow {\n  readonly key: string\n  readonly binding: CcBinding\n  readonly index: number\n  readonly target: CcMappingListTarget | null\n  readonly inputUnavailable: boolean\n  readonly duplicate: boolean\n  readonly fanout: boolean\n}\n\nexport interface CcMappingListGroup {\n  readonly id: string\n  readonly label: string\n  readonly rows: readonly CcMappingListRow[]\n}\n\nfunction targetCatalog(\n  subjects: readonly CcMappingListSubject[]\n): Map<string, CcMappingListTarget> {\n  const out = new Map<string, CcMappingListTarget>()\n  for (const subject of subjects) {\n    for (const param of subject.schema.params) {\n      out.set(`${subject.id ?? \"\"}\\u001f${param.path}`, {\n        subject: subject.label,\n        label: param.label,\n        ...(param.group ? { group: param.group } : {}),\n        min: param.type === \"float\" ? (param.min ?? 0) : 0,\n        max:\n          param.type === \"enum\"\n            ? Math.max(0, (param.options?.length ?? 1) - 1)\n            : param.type === \"bool\"\n              ? 1\n              : (param.max ?? 1),\n        ...(param.type === \"float\" && param.units ? { unit: param.units } : {}),\n        ...(param.type === \"float\" && param.taper && param.taper !== \"linear\"\n          ? { taper: param.taper }\n          : {}),\n      })\n    }\n  }\n  return out\n}\n\n/** \"Any input · omni\", \"KeyStep · ch 2\", \"Unavailable input · ch 2\". */\nexport function ccInputSummary(\n  binding: Pick<CcBinding, \"sourceId\" | \"channel\">,\n  inputLabels: Readonly<Record<string, string>> = {}\n): string {\n  const input = binding.sourceId\n    ? (inputLabels[binding.sourceId] ?? \"Unavailable input\")\n    : \"Any input\"\n  return `${input} · ${binding.channel ? `ch ${binding.channel}` : \"omni\"}`\n}\n\nfunction formatNumber(value: number): string {\n  return Number.isInteger(value)\n    ? String(value)\n    : String(Number(value.toFixed(3)))\n}\n\n/** The range a binding writes, e.g. \"20–20000 Hz\" or \"Inverted · 1–0\". */\nexport function ccRangeSummary(\n  binding: Pick<CcBinding, \"min\" | \"max\" | \"curve\">,\n  target: Pick<CcMappingListTarget, \"min\" | \"max\" | \"unit\"> | null\n): string {\n  const min = binding.min ?? target?.min ?? 0\n  const max = binding.max ?? target?.max ?? 1\n  const unit = target?.unit ? ` ${target.unit}` : \"\"\n  const span = `${formatNumber(min)}–${formatNumber(max)}${unit}`\n  const parts = [min > max ? `Inverted · ${span}` : span]\n  if (binding.curve && binding.curve !== \"linear\") parts.push(binding.curve)\n  return parts.join(\" · \")\n}\n\n/**\n * The list's rows, filtered, sorted and grouped. Pure, so the view rules are\n * pinned without a DOM.\n *\n * \"subject\" groups by module or rack instance (rows by target label); \"cc\"\n * and \"source\" produce one flat group sorted by controller number or input.\n */\nexport function ccMappingListGroups(input: {\n  readonly map: CcMap\n  readonly subjects: readonly CcMappingListSubject[]\n  readonly inputLabels?: Readonly<Record<string, string>>\n  readonly query?: string\n  readonly filter?: CcMappingListFilter\n  readonly sort?: CcMappingListSort\n}): CcMappingListGroup[] {\n  const { map, subjects, inputLabels = {} } = input\n  const filter = input.filter ?? \"all\"\n  const sort = input.sort ?? \"subject\"\n  const catalog = targetCatalog(subjects)\n  const conflicts = analyzeCcMap(map)\n  const indexesOf = (kind: string) =>\n    new Set(\n      conflicts\n        .filter((conflict) => conflict.kind === kind)\n        .flatMap((conflict) => conflict.bindingIndexes)\n    )\n  const duplicateTargets = new Set(\n    [...indexesOf(\"duplicate-target\")].map((index) =>\n      ccBindingTargetKey(map.bindings[index]!)\n    )\n  )\n  const fanoutSources = new Set(\n    [...indexesOf(\"source-fanout\")].map((index) =>\n      ccBindingSourceKey(map.bindings[index]!)\n    )\n  )\n  const needle = (input.query ?? \"\").trim().toLowerCase()\n  const rows: CcMappingListRow[] = map.bindings.flatMap((binding, index) => {\n    const target =\n      catalog.get(`${binding.subjectId ?? \"\"}\\u001f${binding.path}`) ?? null\n    const duplicate = duplicateTargets.has(ccBindingTargetKey(binding))\n    const row: CcMappingListRow = {\n      key: `${ccMappingRowKey(binding)}\\u001f${index}`,\n      binding,\n      index,\n      target,\n      inputUnavailable:\n        binding.sourceId !== undefined && !(binding.sourceId in inputLabels),\n      duplicate,\n      fanout: fanoutSources.has(ccBindingSourceKey(binding)),\n    }\n    if (filter === \"enabled\" && binding.enabled === false) return []\n    if (filter === \"disabled\" && binding.enabled !== false) return []\n    if (filter === \"conflicts\" && !duplicate) return []\n    if (\n      needle &&\n      ![\n        target?.subject,\n        target?.label,\n        target?.group,\n        binding.path,\n        `CC ${binding.cc}`,\n        binding.role ? ccRoleShortLabel(binding.role) : undefined,\n        binding.role ? ccRoleName(binding.role) : undefined,\n        ccInputSummary(binding, inputLabels),\n        binding.valueMode ?? \"absolute\",\n        binding.takeover ?? \"jump\",\n      ].some((value) => value?.toLowerCase().includes(needle))\n    )\n      return []\n    return [row]\n  })\n  const inputOf = (row: CcMappingListRow) =>\n    ccInputSummary(row.binding, inputLabels)\n  const targetOf = (row: CcMappingListRow) =>\n    row.target?.label ?? row.binding.path\n  if (sort !== \"subject\") {\n    const sorted = [...rows].sort((left, right) =>\n      sort === \"cc\"\n        ? left.binding.cc - right.binding.cc ||\n          targetOf(left).localeCompare(targetOf(right))\n        : inputOf(left).localeCompare(inputOf(right)) ||\n          left.binding.cc - right.binding.cc\n    )\n    return sorted.length > 0\n      ? [{ id: \"all\", label: \"All mappings\", rows: sorted }]\n      : []\n  }\n  const groups = new Map<string, { label: string; rows: CcMappingListRow[] }>()\n  const order = [\n    ...subjects.map((subject) => subject.id ?? \"\"),\n    \"\\u0000unresolved\",\n  ]\n  for (const row of rows) {\n    const known = subjects.find(\n      (subject) => (subject.id ?? \"\") === (row.binding.subjectId ?? \"\")\n    )\n    const id = known ? (known.id ?? \"\") : \"\\u0000unresolved\"\n    const label = known ? known.label : \"Unavailable module\"\n    const group = groups.get(id) ?? { label, rows: [] }\n    group.rows.push(row)\n    groups.set(id, group)\n  }\n  return order.flatMap((id) => {\n    const group = groups.get(id)\n    if (!group) return []\n    return [\n      {\n        id: id || \"module\",\n        label: group.label,\n        rows: [...group.rows].sort(\n          (left, right) =>\n            targetOf(left).localeCompare(targetOf(right)) ||\n            left.binding.cc - right.binding.cc\n        ),\n      },\n    ]\n  })\n}\n\nfunction downloadMap(map: CcMap): void {\n  if (typeof document === \"undefined\") return\n  const blob = new Blob([`${JSON.stringify(map, null, 2)}\\n`], {\n    type: \"application/json\",\n  })\n  const url = URL.createObjectURL(blob)\n  const anchor = document.createElement(\"a\")\n  anchor.href = url\n  anchor.download = \"midi-mappings.json\"\n  anchor.click()\n  URL.revokeObjectURL(url)\n}\n\nconst muted =\n  \"var(--uikit-text-muted, color-mix(in srgb, currentColor 62%, transparent))\"\nconst border = `1px solid ${token.border}`\n\nconst fieldStyle: CSSProperties = {\n  boxSizing: \"border-box\",\n  minWidth: 0,\n  height: 30,\n  border,\n  borderRadius: 6,\n  background: \"transparent\",\n  color: \"inherit\",\n  font: \"inherit\",\n  fontSize: 12,\n  padding: \"0 6px\",\n}\n\nfunction buttonStyle(tone: \"plain\" | \"danger\" = \"plain\"): CSSProperties {\n  return {\n    appearance: \"none\",\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    gap: 4,\n    minHeight: 28,\n    border,\n    borderRadius: 6,\n    background: \"transparent\",\n    color: tone === \"danger\" ? token.danger : \"inherit\",\n    font: \"inherit\",\n    fontSize: 12,\n    padding: \"0 8px\",\n    cursor: \"pointer\",\n    whiteSpace: \"nowrap\",\n  }\n}\n\nconst labelStyle: CSSProperties = {\n  display: \"grid\",\n  gap: 4,\n  minWidth: 0,\n  fontSize: 11,\n  fontWeight: 600,\n}\n\n/** The list. */\nexport function CcMappingList({\n  map,\n  subjects,\n  activity = {},\n  inputLabels = {},\n  learnAvailable,\n  learnUnavailableReason,\n  capabilities = {},\n  onPatch,\n  onRemove,\n  onLearn,\n  onPickTarget,\n  onResetDefaults,\n  onClearAll,\n  onImport,\n  onExport,\n  focusKey,\n  toolbar,\n}: CcMappingListProps) {\n  const [query, setQuery] = useState(\"\")\n  const [filter, setFilter] = useState<CcMappingListFilter>(\"all\")\n  const [sort, setSort] = useState<CcMappingListSort>(\"subject\")\n  const [expanded, setExpanded] = useState<string | null>(null)\n  const [advanced, setAdvanced] = useState<string | null>(null)\n  const [importError, setImportError] = useState<string | null>(null)\n  const fileRef = useRef<HTMLInputElement | null>(null)\n  const rootRef = useRef<HTMLDivElement | null>(null)\n\n  const groups = useMemo(\n    () =>\n      ccMappingListGroups({ map, subjects, inputLabels, query, filter, sort }),\n    [filter, inputLabels, map, query, sort, subjects]\n  )\n  const shown = groups.reduce((sum, group) => sum + group.rows.length, 0)\n  const sourceIdentity = capabilities.sourceIdentity !== false\n  const shaping = capabilities.shaping !== false\n  const learnOffered = capabilities.learn !== false && onLearn !== undefined\n\n  // Reopening after a Pick control lands on the row it was about.\n  const focusRowKey = useMemo(() => {\n    if (!focusKey) return null\n    for (const group of groups)\n      for (const row of group.rows)\n        if (row.key.startsWith(focusKey)) return row.key\n    return null\n  }, [focusKey, groups])\n  useEffect(() => {\n    if (!focusKey) return\n    const root = rootRef.current\n    // A control with no mapping has no row: land on search instead.\n    if (!focusRowKey) {\n      root?.querySelector<HTMLElement>('input[type=\"search\"]')?.focus()\n      return\n    }\n    setExpanded(focusRowKey)\n    const button = root?.querySelector<HTMLElement>(\n      `[data-cc-mapping-row=\"${CSS.escape(focusRowKey)}\"] [data-cc-mapping-expand]`\n    )\n    button?.focus()\n    // Only on a new focus request, not on every map edit.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [focusKey])\n\n  return (\n    <div\n      ref={rootRef}\n      data-uikit-cc-mapping-list=\"\"\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 10,\n        minWidth: 0,\n        fontSize: 12,\n      }}\n    >\n      <div\n        role=\"toolbar\"\n        aria-label=\"MIDI mapping tools\"\n        style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, minWidth: 0 }}\n      >\n        <input\n          type=\"search\"\n          value={query}\n          onChange={(event) => setQuery(event.target.value)}\n          placeholder=\"Search target, input or CC\"\n          aria-label=\"Search MIDI mappings\"\n          style={{ ...fieldStyle, flex: \"1 1 12rem\" }}\n        />\n        <select\n          aria-label=\"Filter MIDI mappings\"\n          value={filter}\n          onChange={(event) =>\n            setFilter(event.target.value as CcMappingListFilter)\n          }\n          style={{ ...fieldStyle, flex: \"0 1 auto\" }}\n        >\n          <option value=\"all\">All mappings</option>\n          <option value=\"enabled\">Enabled</option>\n          <option value=\"disabled\">Disabled</option>\n          <option value=\"conflicts\">Conflicts</option>\n        </select>\n        <select\n          aria-label=\"Sort MIDI mappings\"\n          value={sort}\n          onChange={(event) => setSort(event.target.value as CcMappingListSort)}\n          style={{ ...fieldStyle, flex: \"0 1 auto\" }}\n        >\n          <option value=\"subject\">By module</option>\n          <option value=\"cc\">By CC number</option>\n          <option value=\"source\">By input</option>\n        </select>\n        <div\n          style={{\n            display: \"flex\",\n            flexWrap: \"wrap\",\n            gap: 6,\n            flex: \"1 1 100%\",\n          }}\n        >\n          {toolbar}\n          {onImport ? (\n            <>\n              <input\n                ref={fileRef}\n                type=\"file\"\n                accept=\"application/json,.json\"\n                hidden\n                onChange={(event) => {\n                  const file = event.target.files?.[0]\n                  event.currentTarget.value = \"\"\n                  if (!file) return\n                  setImportError(null)\n                  void file\n                    .text()\n                    .then((text) => {\n                      onImport(JSON.parse(text))\n                      setExpanded(null)\n                    })\n                    .catch(() =>\n                      setImportError(\"This mapping file is not valid JSON.\")\n                    )\n                }}\n              />\n              <button\n                type=\"button\"\n                style={buttonStyle()}\n                onClick={() => fileRef.current?.click()}\n              >\n                Import\n              </button>\n            </>\n          ) : null}\n          <button\n            type=\"button\"\n            style={buttonStyle()}\n            disabled={map.bindings.length === 0}\n            onClick={() => (onExport ?? downloadMap)(map)}\n          >\n            Export\n          </button>\n          {onResetDefaults ? (\n            <button\n              type=\"button\"\n              style={buttonStyle()}\n              onClick={() => {\n                setExpanded(null)\n                onResetDefaults()\n              }}\n            >\n              Reset to defaults\n            </button>\n          ) : null}\n          {onClearAll ? (\n            <button\n              type=\"button\"\n              style={buttonStyle(\"danger\")}\n              disabled={map.bindings.length === 0}\n              onClick={() => {\n                setExpanded(null)\n                onClearAll()\n              }}\n            >\n              Clear all\n            </button>\n          ) : null}\n        </div>\n      </div>\n\n      {importError ? (\n        <p role=\"alert\" style={{ margin: 0, color: token.danger }}>\n          {importError}\n        </p>\n      ) : null}\n      {!learnAvailable && learnUnavailableReason ? (\n        <p\n          data-midi-learn-unavailable=\"\"\n          style={{ margin: 0, color: muted, fontSize: 11 }}\n        >\n          {learnUnavailableReason}\n        </p>\n      ) : null}\n\n      <p style={{ margin: 0, color: muted, fontSize: 11 }} aria-live=\"polite\">\n        {map.bindings.length === 0\n          ? \"No mappings yet.\"\n          : shown === map.bindings.length\n            ? `${map.bindings.length} ${map.bindings.length === 1 ? \"mapping\" : \"mappings\"}`\n            : `Showing ${shown} of ${map.bindings.length} mappings`}\n      </p>\n\n      {groups.map((group) => (\n        <section\n          key={group.id}\n          aria-label={group.label}\n          style={{ display: \"grid\", gap: 4, minWidth: 0 }}\n        >\n          <h3\n            style={{\n              margin: \"4px 0 0\",\n              fontSize: 11,\n              fontWeight: 700,\n              letterSpacing: \"0.04em\",\n              textTransform: \"uppercase\",\n              color: muted,\n            }}\n          >\n            {group.label}\n          </h3>\n          <ul\n            style={{\n              listStyle: \"none\",\n              margin: 0,\n              padding: 0,\n              display: \"grid\",\n              gap: 4,\n            }}\n          >\n            {group.rows.map((row) => (\n              <MappingRow\n                key={row.key}\n                row={row}\n                showSubject={sort !== \"subject\"}\n                activity={activity}\n                inputLabels={inputLabels}\n                expanded={expanded === row.key}\n                advanced={advanced === row.key}\n                onToggle={() =>\n                  setExpanded((current) =>\n                    current === row.key ? null : row.key\n                  )\n                }\n                onToggleAdvanced={() =>\n                  setAdvanced((current) =>\n                    current === row.key ? null : row.key\n                  )\n                }\n                sourceIdentity={sourceIdentity}\n                shaping={shaping}\n                learnAvailable={learnAvailable}\n                learnUnavailableReason={learnUnavailableReason ?? null}\n                onPatch={onPatch}\n                onRemove={onRemove}\n                {...(learnOffered ? { onLearn } : {})}\n                {...(onPickTarget ? { onPickTarget } : {})}\n              />\n            ))}\n          </ul>\n        </section>\n      ))}\n      {map.bindings.length > 0 && shown === 0 ? (\n        <p style={{ margin: 0, color: muted }}>\n          No matching mappings. Change the search or filter.\n        </p>\n      ) : null}\n    </div>\n  )\n}\n\nfunction MappingRow({\n  row,\n  showSubject,\n  activity,\n  inputLabels,\n  expanded,\n  advanced,\n  onToggle,\n  onToggleAdvanced,\n  sourceIdentity,\n  shaping,\n  learnAvailable,\n  learnUnavailableReason,\n  onPatch,\n  onRemove,\n  onLearn,\n  onPickTarget,\n}: {\n  row: CcMappingListRow\n  showSubject: boolean\n  activity: Readonly<Record<number, number>>\n  inputLabels: Readonly<Record<string, string>>\n  expanded: boolean\n  advanced: boolean\n  onToggle: () => void\n  onToggleAdvanced: () => void\n  sourceIdentity: boolean\n  shaping: boolean\n  learnAvailable: boolean\n  learnUnavailableReason: string | null\n  onPatch: (binding: CcBinding, patch: CcBindingPatch) => void\n  onRemove: (binding: CcBinding) => void\n  onLearn?: (binding: CcBinding) => void\n  onPickTarget?: (binding: CcBinding) => void\n}) {\n  const { binding, target } = row\n  const name = target?.label ?? binding.path\n  const disabled = binding.enabled === false\n  const live = Object.hasOwn(activity, binding.cc)\n  const unresolved = target === null\n  const patch = (next: CcBindingPatch) => onPatch(binding, next)\n  const detailId = `cc-mapping-detail-${row.index}`\n\n  return (\n    <li\n      data-cc-mapping-row={row.key}\n      data-cc-mapping-unresolved={\n        unresolved || row.inputUnavailable || undefined\n      }\n      style={{\n        border,\n        borderRadius: 8,\n        padding: \"6px 8px\",\n        minWidth: 0,\n        opacity: disabled ? 0.6 : 1,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexWrap: \"wrap\",\n          alignItems: \"center\",\n          gap: \"4px 10px\",\n          minWidth: 0,\n        }}\n      >\n        <span\n          style={{\n            display: \"inline-flex\",\n            alignItems: \"center\",\n            gap: 5,\n            fontFamily: token.fontMono,\n            fontWeight: 700,\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          <span\n            aria-hidden\n            data-cc-mapping-activity={live ? activity[binding.cc] : undefined}\n            style={{\n              width: 6,\n              height: 6,\n              borderRadius: 3,\n              background: live ? token.accent : token.border,\n            }}\n          />\n          {/* A role row reads as its role (\"MW\"): it follows the player's mod\n              wheel on any device, whatever its controller number. */}\n          {binding.role ? (\n            <span\n              data-cc-mapping-role={binding.role}\n              title={`${ccRoleName(binding.role)} (CC ${binding.cc} by default)`}\n            >\n              {ccRoleShortLabel(binding.role)}\n            </span>\n          ) : (\n            <>CC {binding.cc}</>\n          )}\n        </span>\n        <span style={{ minWidth: 0, flex: \"1 1 10rem\" }}>\n          <span style={{ display: \"block\", fontWeight: 600 }}>\n            {name}\n            {unresolved ? (\n              <span style={{ color: token.danger, fontWeight: 500 }}>\n                {\" \"}\n                · Unresolved\n              </span>\n            ) : null}\n            {disabled ? (\n              <span style={{ color: muted, fontWeight: 500 }}> · Off</span>\n            ) : null}\n          </span>\n          <span\n            style={{\n              display: \"block\",\n              color: muted,\n              fontSize: 11,\n              overflowWrap: \"anywhere\",\n            }}\n          >\n            {showSubject && target ? `${target.subject} · ` : \"\"}\n            {binding.path}\n          </span>\n        </span>\n        <span style={{ color: muted, fontSize: 11, minWidth: 0 }}>\n          <span style={{ display: \"block\" }}>\n            {ccInputSummary(binding, inputLabels)}\n            {row.inputUnavailable ? \" · unresolved\" : \"\"}\n          </span>\n          <span style={{ display: \"block\" }}>\n            {ccRangeSummary(binding, target)}\n          </span>\n        </span>\n        <span\n          style={{\n            display: \"flex\",\n            flexWrap: \"wrap\",\n            gap: 4,\n            marginLeft: \"auto\",\n          }}\n        >\n          {onLearn ? (\n            <button\n              type=\"button\"\n              style={buttonStyle()}\n              disabled={!learnAvailable}\n              aria-description={learnUnavailableReason ?? undefined}\n              aria-label={`Learn MIDI CC for ${name}`}\n              onClick={() => onLearn(binding)}\n            >\n              Learn\n            </button>\n          ) : null}\n          {onPickTarget && !unresolved ? (\n            <button\n              type=\"button\"\n              style={buttonStyle()}\n              aria-label={`Pick a new control for CC ${binding.cc}`}\n              onClick={() => onPickTarget(binding)}\n            >\n              Pick control\n            </button>\n          ) : null}\n          <button\n            type=\"button\"\n            style={buttonStyle()}\n            aria-pressed={!disabled}\n            aria-label={`${disabled ? \"Enable\" : \"Disable\"} mapping for ${name}`}\n            onClick={() => patch({ enabled: disabled ? undefined : false })}\n          >\n            {disabled ? \"Enable\" : \"Disable\"}\n          </button>\n          <button\n            type=\"button\"\n            style={buttonStyle(\"danger\")}\n            aria-label={`Remove mapping for ${name}`}\n            onClick={() => onRemove(binding)}\n          >\n            Remove\n          </button>\n          <button\n            type=\"button\"\n            data-cc-mapping-expand=\"\"\n            style={buttonStyle()}\n            aria-expanded={expanded}\n            aria-controls={detailId}\n            aria-label={`Edit mapping for ${name}`}\n            onClick={onToggle}\n          >\n            {expanded ? \"Done\" : \"Edit\"}\n          </button>\n        </span>\n      </div>\n      {row.duplicate || row.fanout ? (\n        <p\n          style={{\n            margin: \"4px 0 0\",\n            fontSize: 11,\n            color: row.duplicate ? token.danger : muted,\n          }}\n        >\n          {row.duplicate\n            ? \"Conflict: more than one mapping writes this target.\"\n            : \"Shared source: this controller drives several targets.\"}\n        </p>\n      ) : null}\n      {expanded ? (\n        <div\n          id={detailId}\n          style={{\n            display: \"grid\",\n            gridTemplateColumns: \"repeat(auto-fit, minmax(7rem, 1fr))\",\n            gap: 8,\n            marginTop: 8,\n            paddingTop: 8,\n            borderTop: border,\n          }}\n        >\n          <label style={labelStyle}>\n            CC number\n            <input\n              type=\"number\"\n              min={0}\n              max={127}\n              step={1}\n              value={binding.cc}\n              aria-label={`MIDI CC number for ${name}`}\n              onChange={(event) => {\n                const value = Number(event.target.value)\n                if (Number.isFinite(value)) patch({ cc: value })\n              }}\n              style={fieldStyle}\n            />\n          </label>\n          <label style={labelStyle}>\n            Channel\n            <select\n              aria-label={`MIDI channel for ${name}`}\n              value={binding.channel ?? \"\"}\n              onChange={(event) =>\n                patch({\n                  channel: event.target.value\n                    ? Number(event.target.value)\n                    : undefined,\n                })\n              }\n              style={fieldStyle}\n            >\n              <option value=\"\">Omni</option>\n              {Array.from({ length: 16 }, (_, index) => index + 1).map(\n                (channel) => (\n                  <option key={channel} value={channel}>\n                    Ch {channel}\n                  </option>\n                )\n              )}\n            </select>\n          </label>\n          {shaping ? (\n            <>\n              <label style={labelStyle}>\n                Minimum\n                <input\n                  type=\"number\"\n                  aria-label={`Minimum value for ${name}`}\n                  value={binding.min ?? \"\"}\n                  placeholder={String(target?.min ?? 0)}\n                  onChange={(event) =>\n                    patch({\n                      min:\n                        event.target.value === \"\"\n                          ? undefined\n                          : Number(event.target.value),\n                    })\n                  }\n                  style={fieldStyle}\n                />\n              </label>\n              <label style={labelStyle}>\n                Maximum\n                <input\n                  type=\"number\"\n                  aria-label={`Maximum value for ${name}`}\n                  value={binding.max ?? \"\"}\n                  placeholder={String(target?.max ?? 1)}\n                  onChange={(event) =>\n                    patch({\n                      max:\n                        event.target.value === \"\"\n                          ? undefined\n                          : Number(event.target.value),\n                    })\n                  }\n                  style={fieldStyle}\n                />\n              </label>\n            </>\n          ) : null}\n          <div style={{ gridColumn: \"1 / -1\" }}>\n            <button\n              type=\"button\"\n              style={{ ...buttonStyle(), border: \"none\", padding: 0 }}\n              aria-expanded={advanced}\n              onClick={onToggleAdvanced}\n            >\n              {advanced ? \"▾\" : \"▸\"} Advanced\n            </button>\n          </div>\n          {advanced ? (\n            <>\n              {sourceIdentity ? (\n                <label style={labelStyle}>\n                  MIDI input\n                  <select\n                    aria-label={`MIDI input for ${name}`}\n                    value={binding.sourceId ?? \"\"}\n                    onChange={(event) =>\n                      patch({ sourceId: event.target.value || undefined })\n                    }\n                    style={fieldStyle}\n                  >\n                    <option value=\"\">Any input</option>\n                    {binding.sourceId && row.inputUnavailable ? (\n                      <option value={binding.sourceId}>\n                        Unavailable input\n                      </option>\n                    ) : null}\n                    {Object.entries(inputLabels).map(([id, label]) => (\n                      <option key={id} value={id}>\n                        {label}\n                      </option>\n                    ))}\n                  </select>\n                </label>\n              ) : null}\n              {shaping ? (\n                <>\n                  <label style={labelStyle}>\n                    Value mode\n                    <select\n                      aria-label={`Input mode for ${name}`}\n                      value={binding.valueMode ?? \"absolute\"}\n                      onChange={(event) =>\n                        patch({\n                          valueMode:\n                            event.target.value === \"absolute\"\n                              ? undefined\n                              : (event.target\n                                  .value as CcBindingPatch[\"valueMode\"]),\n                        })\n                      }\n                      style={fieldStyle}\n                    >\n                      <option value=\"absolute\">Absolute</option>\n                      <option value=\"relative-signed-bit\">\n                        Relative signed bit\n                      </option>\n                      <option value=\"relative-binary-offset\">\n                        Relative binary offset\n                      </option>\n                      <option value=\"relative-twos-complement\">\n                        Relative two&apos;s complement\n                      </option>\n                      <option value=\"toggle\">Toggle</option>\n                    </select>\n                  </label>\n                  {(binding.valueMode ?? \"absolute\") === \"absolute\" ? (\n                    <label style={labelStyle}>\n                      Takeover\n                      <select\n                        aria-label={`Takeover mode for ${name}`}\n                        value={binding.takeover ?? \"jump\"}\n                        onChange={(event) =>\n                          patch({\n                            takeover:\n                              event.target.value === \"jump\"\n                                ? undefined\n                                : (event.target\n                                    .value as CcBindingPatch[\"takeover\"]),\n                          })\n                        }\n                        style={fieldStyle}\n                      >\n                        <option value=\"jump\">Jump</option>\n                        <option value=\"pickup\">Pickup</option>\n                        <option value=\"scaled\">Scaled</option>\n                      </select>\n                    </label>\n                  ) : null}\n                  <label style={labelStyle}>\n                    Curve\n                    <select\n                      aria-label={`Response curve for ${name}`}\n                      value={binding.curve ?? \"linear\"}\n                      onChange={(event) =>\n                        patch({\n                          curve:\n                            event.target.value === \"linear\"\n                              ? undefined\n                              : (event.target.value as CcBindingPatch[\"curve\"]),\n                        })\n                      }\n                      style={fieldStyle}\n                    >\n                      <option value=\"linear\">Linear</option>\n                      <option value=\"exp\">Exp</option>\n                      <option value=\"log\">Log</option>\n                      <option value=\"s-curve\">S-curve</option>\n                    </select>\n                  </label>\n                  <div style={{ ...labelStyle, alignContent: \"end\" }}>\n                    <button\n                      type=\"button\"\n                      style={buttonStyle()}\n                      aria-label={`Invert range for ${name}`}\n                      onClick={() =>\n                        patch({\n                          min: binding.max ?? target?.max ?? 1,\n                          max: binding.min ?? target?.min ?? 0,\n                        })\n                      }\n                    >\n                      Invert range\n                    </button>\n                  </div>\n                </>\n              ) : null}\n            </>\n          ) : null}\n        </div>\n      ) : null}\n    </li>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/cc-mapping-list.tsx"
    }
  ]
}
