{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-chord-readout",
  "type": "registry:ui",
  "title": "Chord Readout",
  "description": "Everything analysis can say about what is sounding, in one card: the chord symbol with its alternatives, note names and the raw set encoding, the Forte set class, the interval-vector chart inline, and a self-consonance meter. The symbol answers what you are playing; everything below answers why it sounds like that. Chord naming is dependency-free — a ranked search over a quality dictionary that reads inversions from the bass note.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-interval-vector-chart.json"
  ],
  "files": [
    {
      "path": "components/ui/chord-readout.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/chord-readout\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 * Chord Readout — everything analysis can say about what is sounding, in one\n * card.\n *\n * The composite view: a chord symbol big enough to read at a glance, then the\n * supporting detail underneath in decreasing order of how often you need it —\n * note names and the raw set encoding, the Forte set class, the interval-vector\n * chart, a self-consonance meter, and optionally the set-class property flags.\n *\n * It is arranged that way on purpose. The symbol answers \"what am I playing\",\n * which is the question 90% of the time; everything below answers \"why does it\n * sound like that\", which is the question you ask occasionally and want in full\n * when you do. A player glances at the top; an analyst reads down.\n *\n * The two halves come from genuinely different places, and that is the point of\n * putting them together. The chord NAME is tonal thinking — roots, qualities,\n * inversions, a C major triad is a C major triad. The set class is atonal\n * thinking — no root at all, just the interval content, where a major and minor\n * triad are the same shape reflected. Seeing both at once is how you notice that\n * the sonority you reached for by ear has a structure.\n *\n * Composed from the kit rather than reimplementing: `nameChord` for the symbol,\n * `pcSetAnalysis` for the set theory, `IntervalVectorChart` for the fingerprint,\n * and `selfConsonance` for the meter.\n *\n * Ported from codex-music-mono's `packages/ui/src/vue/PitchClassSet.vue` and\n * `apps/aura-daemon-ui/src/panels/ChordReadout.vue` — the fields shown and their\n * order follow those two, merged. Deliberate deviations:\n *\n *   - Chord naming is dependency-free (see `name-chord.ts`); the originals used\n *     `@tonaljs/tonal`.\n *   - The set-class reading distinguishes inverse pairs, so a major triad no\n *     longer displays as \"Minor Chord\" — see `lib/pc-set-analysis`.\n *   - No pitch-class clock or chord-shape diagram. Both exist as their own\n *     registry items (`pitch-class-clock`), and a card that renders everything\n *     is harder to place in a layout than one that does a job.\n *\n * SSR-safe: pure markup computed from props, no measurement, no effects.\n *\n * Styling comes from CSS custom properties — see `lib/control-theme/theme.ts`.\n */\n\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport { selfConsonance } from \"maruaudio/ui/shared/consonance\"\nimport { pcSetAnalysis } from \"maruaudio/ui/shared/pc-set-analysis\"\nimport { IntervalVectorChart } from \"@/components/ui/interval-vector-chart\"\nimport { nameChord } from \"maruaudio/ui/shared/name-chord\"\n\nexport interface ChordReadoutProps {\n  /** The sounding MIDI notes. The lowest is treated as the bass, so inversions name themselves. */\n  notes: ReadonlyArray<number>\n  /** Show the set-class property flags (Mirror, Z-set, and so on). */\n  showFlags?: boolean\n  /** Show the interval-vector chart. */\n  showChart?: boolean\n  /** Show the self-consonance meter. */\n  showConsonance?: boolean\n  /** Accent colour. Defaults to the accent token. */\n  color?: string\n  /** Glyph style for the embedded interval-vector chart. */\n  glyphStyle?: \"musical\" | \"emoji\"\n  /** Overall width. Defaults to filling the container. */\n  width?: number | string\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\n/** One labelled row in the detail list. */\nfunction DetailRow({ label, children }: { label: string; children: ReactNode }) {\n  return (\n    <div style={{ display: \"flex\", gap: 8, alignItems: \"baseline\", lineHeight: 1.5 }}>\n      <span\n        style={{\n          flex: \"0 0 92px\",\n          fontSize: 10,\n          letterSpacing: \"0.06em\",\n          textTransform: \"uppercase\",\n          color: token.textMuted,\n        }}\n      >\n        {label}\n      </span>\n      <span style={{ flex: 1, minWidth: 0, fontSize: 12, color: token.text, wordBreak: \"break-word\" }}>\n        {children}\n      </span>\n    </div>\n  )\n}\n\nexport function ChordReadout({\n  notes,\n  showFlags = false,\n  showChart = true,\n  showConsonance = true,\n  color,\n  glyphStyle = \"musical\",\n  width = \"100%\",\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: ChordReadoutProps) {\n  const accent = color ?? token.accent\n  const analysis = pcSetAnalysis(notes)\n  const naming = nameChord(notes)\n  const sounding = notes.length > 0\n\n  // Self-consonance of the sounding set — 0..1, where a triad sits high and a\n  // cluster low. Meaningless for a single note, which is trivially consonant.\n  const consonance = selfConsonance(analysis.pitchClasses)\n  const showMeter = showConsonance && analysis.cardinality >= 2\n\n  const setClass = analysis.setClass\n  const flags: string[] = []\n  if (setClass) {\n    if (setClass.isMirror) flags.push(\"Mirror\")\n    if (setClass.hasComplement) flags.push(\"Has Complement\")\n    if (setClass.uniqueTranspositions !== 12) {\n      flags.push(`Unique Transpositions: ${setClass.uniqueTranspositions}`)\n    }\n    if (setClass.isInverse) flags.push(\"Inverse\")\n    if (setClass.isZSet) flags.push(\"Z-Set\")\n    if (setClass.zCounterpartOrdinal !== null) {\n      flags.push(`Z Counterpart: ${setClass.zCounterpartOrdinal}`)\n    }\n    if (setClass.isCombinatorial) flags.push(\"Combinatorial\")\n    if (setClass.combRI) flags.push(\"Combinatorial with RI\")\n    if (setClass.allCombinatorial) flags.push(\"All-combinatorial\")\n  }\n\n  return (\n    <div\n      className={`uikit-control${className ? ` ${className}` : \"\"}`}\n      aria-label={ariaLabel ?? \"Chord analysis\"}\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 12,\n        width,\n        padding: 14,\n        boxSizing: \"border-box\",\n        background: token.surface,\n        borderRadius: token.radius,\n        border: `1px solid ${token.border}`,\n        fontFamily: token.font,\n        ...style,\n      }}\n    >\n      {/* (a) The headline: chord symbol and up to two alternatives. */}\n      <div style={{ textAlign: \"center\" }}>\n        <div\n          style={{\n            fontSize: 10,\n            letterSpacing: \"0.08em\",\n            textTransform: \"uppercase\",\n            color: token.textMuted,\n          }}\n        >\n          {sounding ? \"sounding\" : \"silent\"}\n        </div>\n        <div\n          style={{\n            fontSize: naming.name ? 34 : 24,\n            fontWeight: naming.name ? 700 : 600,\n            lineHeight: 1.15,\n            color: naming.name ? token.text : token.textMuted,\n          }}\n        >\n          {naming.name ??\n            (sounding ? analysis.noteNames.join(\" \") : \"—\")}\n        </div>\n        {naming.best && (\n          <div style={{ fontSize: 12, color: token.textMuted }}>{naming.best.quality}</div>\n        )}\n        {!naming.name && sounding && (\n          <div style={{ fontSize: 12, color: token.textMuted }}>\n            {analysis.cardinality} note{analysis.cardinality === 1 ? \"\" : \"s\"} — no named chord\n          </div>\n        )}\n        {naming.alternatives.length > 0 && (\n          <div style={{ marginTop: 2, fontSize: 11, color: token.textMuted }}>\n            also: {naming.alternatives.slice(0, 2).map((a) => a.name).join(\", \")}\n          </div>\n        )}\n      </div>\n\n      {!analysis.empty && (\n        <>\n          {/* (b) Note names, pitch-class set, chroma and set number. */}\n          <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n            <DetailRow label=\"Notes\">{analysis.noteNames.join(\", \")}</DetailRow>\n            <DetailRow label=\"PC set\">\n              {`{${analysis.pitchClasses.join(\", \")}}`}\n            </DetailRow>\n            <DetailRow label=\"Chroma\">\n              <code style={{ fontFamily: token.fontMono, fontSize: 11 }}>{analysis.binary}</code>\n            </DetailRow>\n            <DetailRow label=\"Set number\">{analysis.decimal}</DetailRow>\n\n            {/* (c) Forte set class and prime form. */}\n            {setClass && (\n              <>\n                <DetailRow label=\"Forte\">{setClass.forteName}</DetailRow>\n                <DetailRow label=\"Prime form\">\n                  <code style={{ fontFamily: token.fontMono, fontSize: 11 }}>\n                    [{analysis.prime}]\n                  </code>\n                </DetailRow>\n                {setClass.desc && <DetailRow label=\"Set class\">{setClass.desc}</DetailRow>}\n              </>\n            )}\n          </div>\n\n          {/* (d) The interval-vector chart, composed from its own item. */}\n          {showChart && (\n            <IntervalVectorChart\n              vector={analysis.intervalVector}\n              glyphStyle={glyphStyle}\n              height={64}\n              label=\"interval vector\"\n            />\n          )}\n\n          {/* (e) Self-consonance as a small meter. */}\n          {showMeter && (\n            <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n              <span\n                style={{\n                  flex: \"0 0 92px\",\n                  fontSize: 10,\n                  letterSpacing: \"0.06em\",\n                  textTransform: \"uppercase\",\n                  color: token.textMuted,\n                }}\n              >\n                Consonance\n              </span>\n              <div\n                style={{\n                  flex: 1,\n                  height: 6,\n                  borderRadius: 3,\n                  background: token.track,\n                  overflow: \"hidden\",\n                }}\n              >\n                <div\n                  style={{\n                    width: `${Math.max(0, Math.min(1, consonance)) * 100}%`,\n                    height: \"100%\",\n                    borderRadius: 3,\n                    background: accent,\n                  }}\n                />\n              </div>\n              <span\n                style={{\n                  flex: \"0 0 auto\",\n                  fontFamily: token.fontMono,\n                  fontSize: 11,\n                  fontVariantNumeric: \"tabular-nums\",\n                  color: token.text,\n                }}\n              >\n                {consonance.toFixed(2)}\n              </span>\n            </div>\n          )}\n\n          {/* (f) The set-class property flags. */}\n          {showFlags && flags.length > 0 && (\n            <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>\n              {flags.map((flag) => (\n                <span\n                  key={flag}\n                  style={{\n                    fontSize: 10,\n                    padding: \"2px 7px\",\n                    borderRadius: 999,\n                    border: `1px solid ${token.border}`,\n                    color: token.textMuted,\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  {flag}\n                </span>\n              ))}\n            </div>\n          )}\n        </>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/chord-readout.tsx"
    }
  ]
}
