{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-pc-histogram",
  "type": "registry:ui",
  "title": "Pitch Class Histogram",
  "description": "Twelve bars on a common baseline showing how much each pitch class sounded — the plainest view of the data the circle-of-fifths wheel plots, and the one to reach for when you want to read the numbers rather than see the key. Peak-normalised, with a root marker and a live-chord highlight.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/pc-histogram.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/pc-histogram\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 * Pitch-Class Histogram — how much each of the twelve pitch classes sounded.\n *\n * The plainest possible view of the same data the circle-of-fifths wheel plots,\n * and it earns its place by being plain: on a wheel you compare lengths at\n * twelve different angles, which the eye is bad at, whereas twelve bars on a\n * common baseline are exactly what the eye is good at. Use the wheel to see the\n * KEY, this to read the actual numbers.\n *\n * `weights` is CHROMATIC-indexed — index 0 is C, 11 is B — and PEAK-NORMALISED,\n * so the tallest bar always reaches full height whatever the units: note counts,\n * seconds held, velocity sums. A pitch class that never sounded keeps a visible\n * stub, because \"silent\" and \"not in this data\" should not look identical, and a\n * gap in the row is harder to read than a floor.\n *\n * `rootPc` marks the tonic with a tick beneath its bar, and `activePcs`\n * highlights whatever is sounding right now — together they turn a static\n * summary into a live readout: the histogram is the harmony so far, the\n * highlight is the harmony this instant.\n *\n * No Vue original — this is the bar-chart idiom of codex-music-mono's\n * `VectorClassBarchart.vue` applied to pitch classes rather than interval\n * classes. Written for this kit, so it follows the kit's conventions rather\n * than porting anything.\n *\n * SSR-safe: pure SVG 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 } from \"react\"\nimport { pitchClassName } from \"maruaudio/ui/shared/music-theory-pitch-class\"\nimport { token } from \"maruaudio/ui/shared/theme\"\n\n/** The minimum bar height in pixels, so a silent pitch class still reads as a slot. */\nconst STUB_HEIGHT = 2\n\n/** One bar's rectangle, in a coordinate space `height` tall. */\nexport interface HistogramBar {\n  /** Pitch class, 0..11, chromatic. */\n  pc: number\n  /** Peak-normalised weight, 0..1. */\n  weight: number\n  /** Bar height in pixels, never below the visible stub. */\n  barHeight: number\n  /** Top edge, measured from the top of the plot area — SVG's y direction. */\n  y: number\n}\n\n/**\n * The twelve bar heights for a pitch-class histogram.\n *\n * Pure: no DOM, no React. Peak-normalised, so the largest weight fills `height`\n * and everything else is relative to it; an all-zero histogram comes back as\n * twelve stubs rather than a division by zero. Non-finite and negative entries\n * are read as silence.\n *\n * Only the vertical axis is computed here. Bar WIDTH and horizontal spacing are\n * left to the renderer, which knows how wide it actually is — that is what lets\n * the component be fluid without this function needing a width at all.\n */\nexport function histogramBars(\n  weights: ReadonlyArray<number>,\n  height: number,\n): HistogramBar[] {\n  const plot = Math.max(1, height)\n\n  // Peak-normalise. The epsilon floor keeps an empty histogram finite.\n  let max = 1e-9\n  for (let i = 0; i < 12; i += 1) {\n    const v = weights?.[i]\n    if (typeof v === \"number\" && Number.isFinite(v) && v > max) max = v\n  }\n\n  return Array.from({ length: 12 }, (_, pc) => {\n    const raw = weights?.[pc]\n    const value = typeof raw === \"number\" && Number.isFinite(raw) ? raw : 0\n    const weight = Math.max(0, value / max)\n    // The stub floor is why this is a max() and not a bare multiply.\n    const barHeight = Math.max(STUB_HEIGHT, weight * plot)\n    return { pc, weight, barHeight, y: plot - barHeight }\n  })\n}\n\nexport interface PcHistogramProps {\n  /**\n   * Length-12 pitch-class weights in CHROMATIC order (0 = C … 11 = B),\n   * peak-normalised internally. Units are arbitrary.\n   */\n  weights: ReadonlyArray<number>\n  /**\n   * Pitch classes to highlight — typically the chord sounding right now.\n   * Reduced mod 12, so raw MIDI note numbers work too.\n   */\n  activePcs?: ReadonlyArray<number>\n  /** Bar colour. Defaults to the accent token. */\n  color?: string\n  /** Height of the bars in pixels, excluding the labels beneath them. */\n  height?: number\n  /** Draw the note name under each bar. */\n  showLabels?: boolean\n  /** Mark this pitch class as the root/tonic with a tick beneath its bar. */\n  rootPc?: number\n  /** Overall width. Defaults to filling the container. */\n  width?: number | string\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nexport function PcHistogram({\n  weights,\n  activePcs,\n  color,\n  height = 96,\n  showLabels = true,\n  rootPc,\n  width = \"100%\",\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: PcHistogramProps) {\n  const bars = histogramBars(weights, height)\n  const accent = color ?? token.accent\n  const lit = new Set((activePcs ?? []).map((n) => ((n % 12) + 12) % 12))\n  const root = rootPc === undefined ? undefined : ((rootPc % 12) + 12) % 12\n\n  return (\n    <div\n      className={`uikit-control${className ? ` ${className}` : \"\"}`}\n      role=\"img\"\n      aria-label={ariaLabel ?? \"Pitch-class weights\"}\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-end\",\n        gap: 3,\n        width,\n        padding: 8,\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      {bars.map((bar) => {\n        const active = lit.has(bar.pc)\n        const isRoot = root === bar.pc\n        return (\n          <div\n            key={bar.pc}\n            style={{\n              flex: \"1 1 0\",\n              minWidth: 0,\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              gap: 4,\n            }}\n          >\n            {/* The bar's lane. A fixed-height flex column with the bar pinned\n                to the bottom, so every bar shares one baseline. */}\n            <div\n              style={{\n                height,\n                width: \"100%\",\n                display: \"flex\",\n                alignItems: \"flex-end\",\n              }}\n            >\n              <div\n                style={{\n                  width: \"100%\",\n                  height: bar.barHeight,\n                  borderRadius: 2,\n                  background: accent,\n                  // The ramp keeps a quiet pitch class visible without letting\n                  // it compete with a loud one.\n                  opacity: active ? 1 : 0.3 + 0.5 * bar.weight,\n                  // A ring rather than a colour change, so the highlight does\n                  // not fight the `color` prop.\n                  outline: active ? `1.5px solid ${accent}` : undefined,\n                  outlineOffset: 1,\n                }}\n              />\n            </div>\n\n            {/* The root tick — a short underline that reads as an anchor. */}\n            <div\n              style={{\n                width: \"60%\",\n                height: 2,\n                borderRadius: 1,\n                background: isRoot ? accent : \"transparent\",\n              }}\n            />\n\n            {showLabels && (\n              <span\n                style={{\n                  fontSize: 10,\n                  lineHeight: 1,\n                  color: active || isRoot ? token.text : token.textMuted,\n                  fontWeight: active || isRoot ? 600 : 400,\n                  whiteSpace: \"nowrap\",\n                }}\n              >\n                {pitchClassName(bar.pc, \"unicode-sharp\")}\n              </span>\n            )}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/pc-histogram.tsx"
    }
  ]
}
