{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-chord-map",
  "type": "registry:ui",
  "title": "Chord Map",
  "description": "Controlled twelve-column chord map with a shared piano roll, palette bands, activity trail, tension indicators and per-column seed controls. Hosts own chord generation and playback.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-piano-roll.json"
  ],
  "files": [
    {
      "path": "components/ui/chord-map.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/chord-map\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 type React from \"react\"\nimport {\n  PianoRoll,\n  type PianoRollNote,\n} from \"@/components/ui/piano-roll\"\n\nexport interface ChordMapProps {\n  recipes: readonly (readonly number[] | null | undefined)[]\n  names: readonly string[]\n  weights: readonly number[]\n  seeds?: readonly number[]\n  tensions?: readonly (number | null)[]\n  symbol: (root: number, recipe: readonly number[] | null | undefined) => string\n  voicedIntervals?: (recipe: readonly number[]) => readonly number[]\n  activeRoot?: number\n  preview?: boolean\n  disabled?: boolean\n  basePitch?: number\n  height?: number\n  gutterWidth?: number\n  color?: string\n  classPrefix?: string\n  onReroll?: (root: number, reset?: boolean) => void\n  onHover?: (root: number | null) => void\n  onSelect?: (root: number) => void\n}\nconst black = (pc: number) => [1, 3, 6, 8, 10].includes(pc % 12)\n\nconst CHORD_MAP_STYLES = `.uikit-chord-map-roll {\n  position: relative;\n  margin-top: 10px;\n  --uikit-chord-map-trail: 2000ms;\n}\n/* Activity trail: lit at once, eased out by a transition when released. */\n.uikit-chord-map-roll-trail {\n  position: absolute;\n  top: 0;\n  right: 1px;\n  display: grid;\n  grid-template-columns: repeat(12, minmax(0, 1fr));\n  pointer-events: none;\n}\n.uikit-chord-map-roll-trail i {\n  background: linear-gradient(to top, #aebb97, #aebb9733);\n  opacity: 0;\n  transition: opacity var(--uikit-chord-map-trail) cubic-bezier(0.3, 0.3, 0.6, 1);\n  mix-blend-mode: multiply;\n}\n.uikit-chord-map-roll-trail i.is-on {\n  opacity: 0.38;\n  transition-duration: 60ms;\n}\n.uikit-chord-map-roll.is-preview svg[role=\"img\"] {\n  border-style: dashed !important;\n}\n.uikit-chord-map-roll-labels {\n  display: grid;\n  grid-template-columns: repeat(12, minmax(0, 1fr));\n  gap: 2px;\n  padding-top: 4px;\n}\n.uikit-chord-map-roll-labels > div {\n  transition:\n    border-color var(--uikit-chord-map-trail) ease-out,\n    box-shadow var(--uikit-chord-map-trail) ease-out;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 2px;\n  min-width: 0;\n  padding: 3px 0 4px;\n  border-radius: 6px;\n  background: #fbf9f5;\n  border: 1px solid #e3ded7;\n}\n.uikit-chord-map-roll-labels > div.is-black {\n  background: #ece8f0;\n}\n.uikit-chord-map-roll-labels > div.is-outside {\n  opacity: 0.6;\n}\n.uikit-chord-map-roll-labels > div.is-playing {\n  transition-duration: 60ms;\n  border-color: #7e9269;\n  box-shadow: 0 0 0 2px #aebb9766;\n}\n.uikit-chord-map-roll-input {\n  font-size: 11px;\n  font-weight: 600;\n}\n.uikit-chord-map-roll-labels strong {\n  font-size: 11px;\n  font-weight: 600;\n  max-width: 100%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  color: #4c3f5b;\n}\n/* Per-column tension: width and hue follow 1 − interval consonance. */\n.uikit-chord-map-tension {\n  display: block;\n  width: 70%;\n  height: 4px;\n  border-radius: 2px;\n  background: #e7e3dd;\n  overflow: hidden;\n  position: relative;\n}\n.uikit-chord-map-tension::after {\n  content: \"\";\n  position: absolute;\n  inset: 0 auto 0 0;\n  width: calc(var(--tension) * 100%);\n  background: hsl(calc(110 - var(--tension) * 110) 38% 55%);\n}\n.uikit-chord-map-roll-actions {\n  display: flex;\n  align-items: center;\n  gap: 1px;\n  min-height: 20px;\n}\n.uikit-chord-map-seed {\n  border: 0;\n  background: #ede6f5;\n  border-radius: 6px;\n  padding: 1px 3px;\n  font-size: 8px;\n  color: #6f6386;\n}\n.uikit-chord-map-dice {\n  flex: none;\n  display: grid;\n  place-items: center;\n  width: 20px;\n  height: 20px;\n  padding: 0;\n  border: 1px solid transparent;\n  border-radius: 5px;\n  background: transparent;\n  color: #827298;\n}\n.uikit-chord-map-dice:hover:not(:disabled) {\n  border-color: #c9bddb;\n  background: #ede6f5;\n}\n`\n/** Controlled twelve-column chord map. Hosts own generation, seeds and playback. */\nexport function ChordMap({\n  recipes,\n  names,\n  weights,\n  seeds = [],\n  tensions = [],\n  symbol,\n  voicedIntervals = (recipe) => recipe,\n  activeRoot = -1,\n  preview = false,\n  disabled = false,\n  basePitch = 48,\n  height = 300,\n  gutterWidth = 30,\n  color = \"#a18cbe\",\n  classPrefix = \"uikit-chord-map\",\n  onReroll,\n  onHover,\n  onSelect,\n}: ChordMapProps) {\n  const rollNotes: PianoRollNote[] = recipes.flatMap((chord, pc) =>\n    chord\n      ? voicedIntervals(chord).map((interval) => ({\n          id: `${pc}:${interval}`,\n          midi: basePitch + pc + interval,\n          startBeats: pc + 0.08,\n          durationBeats: 0.84,\n          velocity: weights[pc] > 0 ? 120 : 60,\n          tone: \"accent\" as const,\n          color: interval === 0 ? \"#94a779\" : \"#a18cbe\",\n          label: names[(pc + interval + 24) % 12],\n        }))\n      : []\n  )\n  const rollPitches = rollNotes.map((note) => note.midi)\n  const rollLow = Math.min(basePitch, ...rollPitches) - 1\n  const rollHigh = Math.max(basePitch + 12, ...rollPitches) + 1\n  return (\n    <>\n      {classPrefix === \"uikit-chord-map\" ? (\n        <style>{CHORD_MAP_STYLES}</style>\n      ) : null}\n      <div className={`${classPrefix}-roll ${preview ? \"is-preview\" : \"\"}`}>\n        <PianoRoll\n          notes={rollNotes}\n          bands={weights.map((w, pc) => ({\n            startBeats: pc,\n            durationBeats: 1,\n            strength: Math.min(1, w),\n          }))}\n          bars={12}\n          beatsPerBar={1}\n          subdivisions={1}\n          low={rollLow}\n          high={rollHigh}\n          playhead={activeRoot >= 0 ? activeRoot + 0.5 : null}\n          showPosition={false}\n          gutterWidth={gutterWidth}\n          height={height}\n          color={color}\n          aria-label=\"Chord notes for each played pitch class\"\n        />\n        <div\n          className={`${classPrefix}-roll-trail`}\n          style={{ left: gutterWidth + 1, height: height }}\n          aria-hidden=\"true\"\n        >\n          {names.map((name, pc) => (\n            <i key={name} className={activeRoot === pc ? \"is-on\" : \"\"} />\n          ))}\n        </div>\n        <div\n          className={`${classPrefix}-roll-labels`}\n          style={{ paddingLeft: gutterWidth + 1 }}\n          role=\"list\"\n        >\n          {names.map((name, pc) => {\n            const chord = recipes[pc]\n            const label = symbol(pc, chord)\n            const seed = seeds[pc]\n            return (\n              <div\n                key={name}\n                role=\"listitem\"\n                onMouseEnter={() => onHover?.(pc)}\n                onMouseLeave={() => onHover?.(null)}\n                onFocus={() => onHover?.(pc)}\n                onBlur={() => onHover?.(null)}\n                className={`${black(pc) ? \"is-black\" : \"\"} ${activeRoot === pc ? \"is-playing\" : \"\"} ${weights[pc] === 0 ? \"is-outside\" : \"\"}`}\n                aria-label={`${name}: ${label}; ${chord ? chord.map((i) => names[(pc + i) % 12]).join(\", \") : \"unavailable\"}`}\n              >\n                {onSelect ? (\n                  <button\n                    type=\"button\"\n                    disabled={disabled}\n                    aria-label={`Play ${label}`}\n                    onClick={() => {\n                      if (!disabled) onSelect(pc)\n                    }}\n                    style={{\n                      display: \"flex\",\n                      flexDirection: \"column\",\n                      alignItems: \"center\",\n                      gap: 2,\n                      width: \"100%\",\n                      minWidth: 0,\n                      padding: 0,\n                      border: 0,\n                      background: \"transparent\",\n                      color: \"inherit\",\n                      font: \"inherit\",\n                      cursor: disabled ? \"default\" : \"pointer\",\n                    }}\n                  >\n                    <span className={`${classPrefix}-roll-input`}>{name}</span>\n                    <strong>\n                      {chord ? label.slice(name.length) || \"maj\" : \"—\"}\n                    </strong>\n                  </button>\n                ) : (\n                  <>\n                    <span className={`${classPrefix}-roll-input`}>{name}</span>\n                    <strong>\n                      {chord ? label.slice(name.length) || \"maj\" : \"—\"}\n                    </strong>\n                  </>\n                )}\n                <span\n                  className={`${classPrefix}-tension`}\n                  style={\n                    {\n                      \"--tension\": tensions[pc] ?? 0,\n                    } as React.CSSProperties\n                  }\n                />\n                <span className={`${classPrefix}-roll-actions`}>\n                  <button\n                    className={`${classPrefix}-dice`}\n                    aria-label={`Another chord for ${name}`}\n                    onClick={(event) => {\n                      event.stopPropagation()\n                      if (!disabled) onReroll?.(pc)\n                    }}\n                    disabled={disabled}\n                  >\n                    <span aria-hidden=\"true\">⚄</span>\n                  </button>\n                  {seed > 0 ? (\n                    <button\n                      className={`${classPrefix}-seed`}\n                      disabled={disabled}\n                      onClick={(event) => {\n                        event.stopPropagation()\n                        if (!disabled) onReroll?.(pc, true)\n                      }}\n                      aria-label={`Reset ${name} to the optimized chord`}\n                    >\n                      #{seed}\n                    </button>\n                  ) : null}\n                </span>\n              </div>\n            )\n          })}\n        </div>\n      </div>\n    </>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/chord-map.tsx"
    }
  ]
}
