{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-cc-source",
  "type": "registry:ui",
  "title": "CC Source Strip",
  "description": "The MIDI CC chips you drag onto a control to bind it: the mod wheel first, then the expressive controllers, then a numeric picker for the other hundred-odd numbers. Each chip is draggable by pointer, assignable from the keyboard, and pulses when its controller sends traffic.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/cc-source.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/cc-source\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 * CcSourceChip / CcSourceStrip — the things you drag onto a control.\n *\n * A chip is one MIDI source: the mod wheel, a breath controller, CC 74. Drag it\n * onto a knob or a fader and that control is bound; there is no mapping dialog\n * anywhere in the flow. The chip is also a button — Enter picks it up, the\n * arrow keys walk the controls, Enter drops — so the binding is reachable\n * without a pointer at all.\n *\n * The strip is the palette: the mod wheel first (it is the one every keyboard\n * has and the one players reach for), then whichever sources the instrument\n * cares about, then a picker chip for the other hundred-odd numbers. The picker\n * is deliberately last and deliberately small — typing a controller number is\n * the expert path, not the default one.\n *\n * `activity` is the hardware talking back: pass the raw 0-127 value for a CC and\n * its chip pulses as the value changes, which is how you find out that the pedal\n * you just pressed is CC 64 without reading a manual.\n *\n * `mapped` is the map talking back: a chip whose controller already drives\n * something carries an accent tint and dot, and its tooltip names the targets,\n * so the palette answers \"is Mod in use?\" without reading the mapping list.\n *\n * All drag mechanics live in `lib/cc-dnd`; this file is chrome.\n */\n\nimport { useId, useState } from \"react\"\nimport type { CSSProperties } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  CcDragGhost,\n  ccLabel,\n  ccSourceAriaLabel,\n  useCcActivityPulse,\n  useCcDragSource,\n  type CcAssignment,\n  type CcDndController,\n} from \"maruaudio/ui/shared/cc-dnd\"\n\n/** One entry in the palette. */\nexport interface CcSource {\n  /** Controller number, 0-127. */\n  cc: number\n  /** Display name. Falls back to `CC<n>`. */\n  label?: string\n  /** Explains the MIDI convention or hardware control behind the short label. */\n  description?: string\n}\n\n/** A named hardware/source layout. Selecting one never creates bindings. */\nexport interface CcSourcePalette {\n  id: string\n  label: string\n  description: string\n  sources: ReadonlyArray<CcSource>\n}\n\n/**\n * The controllers worth putting on a panel by default.\n *\n * These are the continuous, expressive ones from the MIDI spec — the numbers a\n * player's hardware actually sends. Switches (sustain, sostenuto) are left out:\n * they send 0 or 127 and mapping one to a knob is rarely what anyone means.\n */\nexport const DEFAULT_CC_SOURCES: ReadonlyArray<CcSource> = [\n  { cc: 1, label: \"Mod\", description: \"Modulation wheel\" },\n  { cc: 2, label: \"Breath\", description: \"Breath controller\" },\n  { cc: 4, label: \"Foot\", description: \"Foot controller\" },\n  { cc: 7, label: \"Volume\", description: \"Channel volume\" },\n  { cc: 10, label: \"Pan\", description: \"Stereo pan\" },\n  { cc: 11, label: \"Expr\", description: \"Expression\" },\n  { cc: 74, label: \"Bright\", description: \"Sound brightness\" },\n]\n\n/**\n * Ready-to-drag layouts, kept in the kit so every module and host names the\n * same hardware the same way. These are source palettes, not parameter maps:\n * changing palette only changes the chips on offer.\n *\n * Kontrol CC14-21 is Native Instruments' documented MIDI-mode page 1 for the\n * eight display knobs on the Kontrol S-Series MK3. Older/editable NI templates\n * can use other numbers, which is why the numeric picker remains alongside it.\n */\nexport const DEFAULT_CC_SOURCE_PALETTES: ReadonlyArray<CcSourcePalette> = [\n  {\n    id: \"common\",\n    label: \"Common controls\",\n    description:\n      \"Standard continuous MIDI controls such as mod, expression and brightness.\",\n    sources: DEFAULT_CC_SOURCES,\n  },\n  {\n    id: \"kontrol-14-21\",\n    label: \"Kontrol 8 knobs\",\n    description:\n      \"Native Instruments Kontrol S-Series MK3 MIDI page 1, CC14-21.\",\n    sources: Array.from({ length: 8 }, (_, index) => ({\n      cc: 14 + index,\n      label: `Knob ${index + 1}`,\n      description: `Kontrol MIDI page 1 knob ${index + 1}`,\n    })),\n  },\n]\n\n/** The mod wheel, named. Exported because it is special-cased everywhere. */\nexport const MOD_WHEEL_SOURCE: CcSource = { cc: 1, label: \"Mod\" }\n\nexport interface CcSourceChipProps {\n  /** Controller number, 0-127. */\n  cc: number\n  /** Display name, e.g. \"Mod\". Shown instead of `CC<n>`. */\n  label?: string\n  /** Longer explanation shown as a tooltip; not stored in the assignment. */\n  description?: string\n  /**\n   * Live traffic for this controller: the raw 0-127 value (any change pulses)\n   * or `true` for a single pulse.\n   */\n  activity?: boolean | number\n  /** What this controller already drives, as display labels. Empty = unmapped. */\n  mappedTo?: ReadonlyArray<string>\n  disabled?: boolean\n  /** Fired after a successful drop, with the target it landed on. */\n  onAssigned?: (assignment: CcAssignment, targetId: string) => void\n  /**\n   * The controller role the host's global role layer gives this number on\n   * any device (\"modWheel\" for CC 1). A drop then creates a role binding,\n   * which follows the player's mod wheel wherever they learned it.\n   */\n  role?: CcAssignment[\"role\"]\n  /** Share a controller other than the ambient one. */\n  controller?: CcDndController\n  className?: string\n  style?: CSSProperties\n}\n\n/** The tooltip/description suffix naming what a controller already drives. */\nfunction mappedSuffix(mappedTo: ReadonlyArray<string> | undefined): string {\n  if (!mappedTo?.length) return \"\"\n  const shown = mappedTo.slice(0, 3).join(\", \")\n  const more = mappedTo.length > 3 ? ` +${mappedTo.length - 3}` : \"\"\n  return `Mapped to ${shown}${more}`\n}\n\n/** The small dot a mapped chip carries, in the mapped colour. */\nfunction MappedDot() {\n  return (\n    <span\n      aria-hidden\n      data-cc-source-mapped-dot=\"\"\n      style={{\n        width: 5,\n        height: 5,\n        flexShrink: 0,\n        borderRadius: 999,\n        background: token.mapped,\n      }}\n    />\n  )\n}\n\n/**\n * One draggable source.\n *\n * The whole chip is the handle — unlike a strip knob, a chip has no competing\n * gesture, so there is nothing for a separate grip to disambiguate. The grip\n * dots are still drawn, because they are what makes a thing look draggable.\n */\nexport function CcSourceChip({\n  cc,\n  label,\n  description,\n  activity,\n  mappedTo,\n  disabled = false,\n  onAssigned,\n  role,\n  controller,\n  className,\n  style,\n}: CcSourceChipProps) {\n  const assignment: CcAssignment = {\n    cc,\n    ...(label !== undefined ? { label } : {}),\n    ...(role ? { role } : {}),\n  }\n  const [hovered, setHovered] = useState(false)\n  const pulsing = useCcActivityPulse(activity)\n\n  const source = useCcDragSource({\n    assignment,\n    disabled,\n    controller,\n    onDropped: onAssigned,\n  })\n\n  const lit = pulsing || source.dragging\n  const raised = hovered && !disabled\n  const mapped = Boolean(mappedTo?.length)\n  const mappedText = mappedSuffix(mappedTo)\n\n  return (\n    <span\n      className={`uikit-control${className ? ` ${className}` : \"\"}`}\n      {...source.handleProps}\n      data-cc-source-mapped={mapped ? \"\" : undefined}\n      aria-description={\n        [description, mappedText].filter(Boolean).join(\". \") || undefined\n      }\n      onPointerEnter={() => setHovered(true)}\n      onPointerLeave={() => setHovered(false)}\n      style={{\n        position: \"relative\",\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: 5,\n        padding: \"4px 7px\",\n        borderRadius: token.radius,\n        background: lit\n          ? `color-mix(in srgb, ${token.accent} 22%, ${token.surface})`\n          : mapped\n            ? `color-mix(in srgb, ${token.mapped} 12%, ${token.surface})`\n            : token.surface,\n        border: `1px solid ${\n          lit\n            ? token.accent\n            : mapped\n              ? `color-mix(in srgb, ${token.mapped} 60%, ${token.border})`\n              : token.border\n        }`,\n        boxShadow: source.dragging\n          ? `0 0 0 1px ${token.accent}, 0 6px 16px rgba(0,0,0,0.45)`\n          : raised\n            ? `0 2px 6px rgba(0,0,0,0.35)`\n            : \"0 1px 2px rgba(0,0,0,0.25)\",\n        color: token.text,\n        fontFamily: token.font,\n        fontSize: 10,\n        fontWeight: 600,\n        letterSpacing: \"0.04em\",\n        whiteSpace: \"nowrap\",\n        cursor: disabled\n          ? \"not-allowed\"\n          : source.dragging\n            ? \"grabbing\"\n            : \"grab\",\n        opacity: disabled ? 0.45 : source.dragging ? 0.9 : 1,\n        transition:\n          \"background 140ms ease-out, border-color 140ms ease-out, box-shadow 140ms ease-out\",\n        userSelect: \"none\",\n        ...style,\n      }}\n    >\n      <GripGlyph />\n      <span style={{ color: lit ? token.accent : token.text }}>\n        {ccLabel(assignment)}\n      </span>\n      {label !== undefined && (\n        <span\n          aria-hidden\n          style={{\n            fontFamily: token.fontMono,\n            fontSize: 8,\n            fontWeight: 700,\n            color: token.textMuted,\n          }}\n        >\n          {cc}\n        </span>\n      )}\n      {mapped ? <MappedDot /> : null}\n      {/* The ghost belongs to the chip that started the drag, so exactly one\n          is rendered no matter how many chips are on screen. */}\n      <CcDragGhost sourceId={source.sourceId} controller={controller} />\n    </span>\n  )\n}\n\nexport interface CcSourceStripProps {\n  /** The palette. Defaults to `DEFAULT_CC_SOURCES`. */\n  sources?: ReadonlyArray<CcSource>\n  /** Optional named layouts. `sources` still wins when explicitly supplied. */\n  palettes?: ReadonlyArray<CcSourcePalette>\n  /** Show the layout selector. Defaults off for backwards-compatible chrome. */\n  palettePicker?: boolean\n  /** Put the mod wheel first when the list does not already contain it. */\n  includeModWheel?: boolean\n  /** Show the numeric picker chip for controllers outside the palette. */\n  picker?: boolean\n  /** Which controller the picker starts on. */\n  pickerDefaultCc?: number\n  /**\n   * Live traffic, keyed by controller number: `{ 1: 64 }` pulses the mod wheel\n   * chip. Feed it straight from a MIDI listener.\n   */\n  activity?: Readonly<Record<number, number>>\n  /**\n   * What each controller already drives, keyed by controller number:\n   * `{ 1: [\"Cutoff\"] }` marks the mod wheel chip as mapped.\n   */\n  mapped?: Readonly<Record<number, ReadonlyArray<string>>>\n  disabled?: boolean\n  /** Fired after any chip lands on a target. */\n  onAssigned?: (assignment: CcAssignment, targetId: string) => void\n  /**\n   * Roles by controller number (`{ 1: \"modWheel\" }`), from the host's global\n   * role layer. A chip with a role drops as a role binding.\n   */\n  roles?: Readonly<Record<number, NonNullable<CcAssignment[\"role\"]>>>\n  controller?: CcDndController\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\n/** The palette row: sources to drag, plus a picker for everything else. */\nexport function CcSourceStrip({\n  sources,\n  palettes = DEFAULT_CC_SOURCE_PALETTES,\n  palettePicker = false,\n  includeModWheel = true,\n  picker = true,\n  pickerDefaultCc = 74,\n  activity,\n  mapped,\n  disabled = false,\n  onAssigned,\n  roles,\n  controller,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: CcSourceStripProps) {\n  const [paletteId, setPaletteId] = useState(palettes[0]?.id ?? \"\")\n  const selectedPalette =\n    palettes.find((candidate) => candidate.id === paletteId) ?? palettes[0]\n  const paletteSources =\n    sources ?? selectedPalette?.sources ?? DEFAULT_CC_SOURCES\n  const list =\n    includeModWheel &&\n    !paletteSources.some((source) => source.cc === MOD_WHEEL_SOURCE.cc)\n      ? [MOD_WHEEL_SOURCE, ...paletteSources]\n      : paletteSources\n\n  return (\n    <div\n      className={className}\n      role=\"group\"\n      aria-label={\n        ariaLabel ?? \"MIDI CC sources — drag onto a control to assign it\"\n      }\n      style={{\n        display: \"flex\",\n        flexWrap: \"wrap\",\n        alignItems: \"center\",\n        gap: 6,\n        fontFamily: token.font,\n        ...style,\n      }}\n    >\n      {/* The set selector is a setting ABOUT the chips, not one of them: its\n          own row, drawn as plain text with the native caret, so nothing that\n          only opens a menu wears the draggable chip's surface and border. */}\n      {palettePicker && !sources && palettes.length > 1 ? (\n        <label\n          data-cc-source-palette=\"\"\n          style={{\n            display: \"flex\",\n            flexBasis: \"100%\",\n            alignItems: \"center\",\n            gap: 4,\n            color: token.textMuted,\n            fontSize: 11,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          Source set\n          <select\n            className=\"uikit-control\"\n            aria-label=\"MIDI controller source set\"\n            value={selectedPalette?.id ?? \"\"}\n            onChange={(event) => setPaletteId(event.target.value)}\n            aria-description={selectedPalette?.description}\n            style={{\n              appearance: \"auto\",\n              border: 0,\n              borderRadius: 4,\n              background: \"transparent\",\n              color: token.text,\n              cursor: \"pointer\",\n              font: \"inherit\",\n              fontWeight: 600,\n              padding: \"1px 0\",\n            }}\n          >\n            {palettes.map((palette) => (\n              <option key={palette.id} value={palette.id}>\n                {palette.label}\n              </option>\n            ))}\n          </select>\n        </label>\n      ) : null}\n      {list.map((source) => (\n        <CcSourceChip\n          key={source.cc}\n          cc={source.cc}\n          label={source.label}\n          description={source.description}\n          activity={activity?.[source.cc]}\n          mappedTo={mapped?.[source.cc]}\n          disabled={disabled}\n          onAssigned={onAssigned}\n          {...(roles?.[source.cc] ? { role: roles[source.cc] } : {})}\n          controller={controller}\n        />\n      ))}\n\n      {picker && (\n        <CcPickerChip\n          defaultCc={pickerDefaultCc}\n          activity={activity}\n          mapped={mapped}\n          disabled={disabled}\n          onAssigned={onAssigned}\n          roles={roles}\n          controller={controller}\n        />\n      )}\n    </div>\n  )\n}\n\ninterface CcPickerChipProps {\n  defaultCc: number\n  activity?: Readonly<Record<number, number>>\n  mapped?: Readonly<Record<number, ReadonlyArray<string>>>\n  disabled: boolean\n  onAssigned?: (assignment: CcAssignment, targetId: string) => void\n  roles?: CcSourceStripProps[\"roles\"]\n  controller?: CcDndController\n}\n\n/**\n * The \"any controller\" chip: type a number, drag the grip.\n *\n * The number field and the drag handle are separate on purpose — a pointerdown\n * on an input must place a caret, not start a drag, so only the grip half\n * carries the gesture. The keyboard path lives on the grip too, which keeps\n * Enter meaning \"pick up\" rather than fighting the field for it.\n */\nfunction CcPickerChip({\n  defaultCc,\n  activity,\n  mapped,\n  disabled,\n  onAssigned,\n  roles,\n  controller,\n}: CcPickerChipProps) {\n  const [cc, setCc] = useState(clampCc(defaultCc))\n  const inputId = useId()\n  const pulsing = useCcActivityPulse(activity?.[cc])\n\n  const assignment: CcAssignment = {\n    cc,\n    ...(roles?.[cc] ? { role: roles[cc] } : {}),\n  }\n  const source = useCcDragSource({\n    assignment,\n    disabled,\n    controller,\n    onDropped: onAssigned,\n  })\n\n  const lit = pulsing || source.dragging\n  const mappedTo = mapped?.[cc]\n  const isMapped = Boolean(mappedTo?.length)\n\n  return (\n    <span\n      data-cc-source-mapped={isMapped ? \"\" : undefined}\n      style={{\n        position: \"relative\",\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: 4,\n        padding: \"3px 5px 3px 7px\",\n        borderRadius: token.radius,\n        background: isMapped\n          ? `color-mix(in srgb, ${token.mapped} 12%, ${token.surface})`\n          : token.surface,\n        border: `1px dashed ${\n          lit\n            ? token.accent\n            : isMapped\n              ? `color-mix(in srgb, ${token.mapped} 60%, ${token.border})`\n              : token.border\n        }`,\n        color: token.textMuted,\n        fontFamily: token.font,\n        fontSize: 10,\n        fontWeight: 600,\n        letterSpacing: \"0.04em\",\n        opacity: disabled ? 0.45 : 1,\n      }}\n    >\n      <label\n        htmlFor={inputId}\n        style={{\n          fontFamily: token.fontMono,\n          fontSize: 9,\n          color: token.textMuted,\n        }}\n      >\n        CC\n      </label>\n      <input\n        id={inputId}\n        type=\"number\"\n        min={0}\n        max={127}\n        step={1}\n        value={cc}\n        disabled={disabled}\n        aria-label=\"Controller number, 0 to 127\"\n        onChange={(event) => setCc(clampCc(Number(event.target.value)))}\n        style={{\n          width: 38,\n          appearance: \"textfield\",\n          background: \"transparent\",\n          border: `1px solid ${token.border}`,\n          borderRadius: 4,\n          color: token.text,\n          fontFamily: token.fontMono,\n          fontSize: 10,\n          fontVariantNumeric: \"tabular-nums\",\n          padding: \"1px 3px\",\n          textAlign: \"center\",\n        }}\n      />\n      <span\n        className=\"uikit-control\"\n        {...source.handleProps}\n        aria-label={ccSourceAriaLabel(assignment)}\n        aria-description={mappedSuffix(mappedTo) || undefined}\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          color: lit ? token.accent : token.textMuted,\n          cursor: disabled\n            ? \"not-allowed\"\n            : source.dragging\n              ? \"grabbing\"\n              : \"grab\",\n          lineHeight: 0,\n          padding: \"2px 1px\",\n        }}\n      >\n        <GripGlyph />\n      </span>\n      {isMapped ? <MappedDot /> : null}\n      <CcDragGhost sourceId={source.sourceId} controller={controller} />\n    </span>\n  )\n}\n\nfunction clampCc(value: number): number {\n  if (!Number.isFinite(value)) return 0\n  return Math.min(127, Math.max(0, Math.round(value)))\n}\n\nfunction GripGlyph() {\n  return (\n    <svg\n      width=\"10\"\n      height=\"12\"\n      viewBox=\"0 0 10 12\"\n      fill=\"currentColor\"\n      aria-hidden\n    >\n      {[2, 6, 10].map((y) =>\n        [2, 7].map((x) => <circle key={`${x}-${y}`} cx={x} cy={y} r=\"1\" />)\n      )}\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/cc-source.tsx"
    }
  ]
}
