{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-ring-roll",
  "type": "registry:ui",
  "title": "Ring Roll",
  "description": "A progression on the seven lanes of a ring, one per ring position from the cut: each chord a run of cells, filled for the ring's notes, ticked where a note is bent, dashed across a gap and hollow for a foreign chord. Common tones between neighbours are bridged into connected bands, the hinge lane is tinted, runs that straddle the cut are split with tie marks and a re-cut phrase gets its own lane labels. One column per chord or width by beats; columns are clickable.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/ring-roll.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/ring-roll\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 * Ring Roll — a progression drawn on the seven lanes of a ring, one per ring\n * position, bottom up from the cut.\n *\n * Two layouts of the same model: `column` (one column per chord, columns\n * touching) and `roll` (width by beats). A chord is a run of cells: filled for\n * the ring's note, ticked and labelled (♭/♯) where a note is bent, dashed where\n * the run has a gap, hollow for a foreign chord (with its ring notation above).\n * A band — a pitch class two neighbours share on the same lane — bridges the\n * gap between their cells, so common tones read as connected rectangles. The\n * hinge lane is tinted; a run that straddles the cut is drawn in two pieces\n * with a tie mark at each edge; a re-cut phrase gets its own lane labels after\n * a dashed boundary.\n *\n * View-only and structural: `roll` is a `RingRollModel` (see\n * `ring-geometry.ts`) whose labels, icons and notation are already chosen. A\n * theory library's roll object can be passed directly when its fields match.\n * Pure SVG from props; columns are keyboard-focusable buttons.\n */\n\nimport type { KeyboardEvent } from \"react\"\nimport {\n  RING_ROLL,\n  ringRollCellStyle,\n  ringRollLaneY,\n  ringRollPlacement,\n  ringRound,\n  type RingRollLayout,\n  type RingRollModel,\n} from \"maruaudio/ui/shared/ring-geometry\"\n\nexport interface RingRollProps {\n  readonly roll: RingRollModel\n  readonly layout: RingRollLayout\n  /** Column index drawn highlighted, or null. */\n  readonly active: number | null\n  readonly onPick: (index: number) => void\n  /** Fill behind a re-cut phrase's lane labels; defaults to the page background. */\n  readonly ground?: string\n}\n\nexport function RingRoll({ roll, layout, active, onPick, ground = \"var(--background, #fff)\" }: RingRollProps) {\n  const { lane: LANE, gutter: GUTTER, top: TOP, gap: GAP, lanes: LANES } = RING_ROLL\n  const r6 = ringRound\n  const { placed, boundaries, width, height } = ringRollPlacement(roll, layout)\n  const yOf = ringRollLaneY\n  const home = roll.phrases[0]\n  const pick = (i: number) => (e: KeyboardEvent) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onPick(i) } }\n  const bottom = TOP + LANES * LANE\n\n  const labels = (phraseIndex: number, x0: number) => roll.phrases[phraseIndex]!.labels.map((l) => (\n    <text key={`l${phraseIndex}-${l.lane}`} x={r6(x0 + GUTTER - 6)} y={r6(yOf(l.lane) + LANE / 2 + 3.5)} textAnchor=\"end\" fontSize={9.5} fontWeight={l.hinge ? 700 : 400} fill=\"currentColor\" opacity={l.hinge ? 1 : 0.75}>\n      {l.hinge ? \"◆ \" : \"\"}{l.label}\n    </text>\n  ))\n\n  return (\n    <svg viewBox={`0 0 ${width} ${height}`} width={width} height={height} role=\"group\" aria-label={`Ring roll, ${roll.columns.length} chords, cut at ${home?.labels[0]?.label ?? \"?\"}`} style={{ display: \"block\", maxWidth: \"none\" }}>\n      {roll.phrases.map((ph, k) => {\n        const lane = ph.labels.find((l) => l.hinge)?.lane\n        const first = placed[ph.start], last = placed[ph.end - 1]\n        if (lane === undefined || !first || !last) return null\n        const x0 = k === 0 ? GUTTER - 2 : first.x\n        return <rect key={`h${k}`} x={r6(x0)} y={yOf(lane)} width={r6(last.x + last.w - x0)} height={LANE} fill=\"currentColor\" opacity={0.07} />\n      })}\n      {Array.from({ length: LANES + 1 }, (_, k) => <line key={`g${k}`} x1={GUTTER - 2} x2={r6(width - 8)} y1={r6(TOP + k * LANE)} y2={r6(TOP + k * LANE)} stroke=\"currentColor\" strokeWidth={0.5} opacity={0.15} />)}\n      {roll.phrases.length > 0 && labels(0, 0)}\n      {boundaries.map((b) => (\n        <g key={`b${b.phrase}`}>\n          <line x1={r6(b.x + 2)} x2={r6(b.x + 2)} y1={TOP - 26} y2={bottom + 4} stroke=\"currentColor\" strokeDasharray=\"3 3\" opacity={0.6} />\n          <rect x={r6(b.x + 4)} y={TOP} width={GUTTER - 6} height={LANES * LANE} fill={ground} opacity={0.9} />\n          {labels(b.phrase, b.x)}\n          <text x={r6(b.x + 6)} y={TOP - 18} fontSize={9} fill=\"currentColor\">{roll.phrases[b.phrase]!.key ? `in ${roll.phrases[b.phrase]!.key}` : \"home\"}</text>\n        </g>\n      ))}\n\n      {placed.slice(0, -1).map((p, i) => {\n        const next = placed[i + 1]!\n        if (next.col.phrase !== p.col.phrase) return null\n        const band = roll.bands.find((b) => b.from === i && b.to === i + 1)\n        return band?.lanes.map((lane) => <rect key={`band${i}-${lane}`} x={r6(p.x + p.w - GAP)} y={r6(yOf(lane) + 4)} width={GAP * 2} height={LANE - 8} fill=\"currentColor\" opacity={0.72} />)\n      })}\n\n      {placed.map(({ col, x: cx, w }) => {\n        const isActive = active === col.index\n        const x0 = r6(cx + GAP / 2), cw = r6(Math.max(2, w - GAP))\n        const mid = r6(cx + w / 2)\n        const title = `${col.symbol ?? \"?\"} ${col.notation}${col.hollow && col.change ? \", foreign\" : \"\"}${col.wraps ? \", wraps the cut\" : \"\"}`\n        return (\n          <g key={`c${col.index}`} role=\"button\" tabIndex={0} aria-pressed={isActive} aria-label={title} onClick={() => onPick(col.index)} onKeyDown={pick(col.index)} style={{ cursor: \"pointer\" }}>\n            <desc>{title}</desc>\n            <rect x={r6(cx)} y={TOP - 30} width={r6(w)} height={LANES * LANE + 58} fill=\"currentColor\" opacity={isActive ? 0.12 : 0} />\n            <text x={mid} y={TOP - 18} textAnchor=\"middle\" fontSize={13} fill=\"currentColor\">{w >= 12 ? col.icon : \"\"}</text>\n            {col.hollow && w >= 30 && <text x={mid} y={TOP - 5} textAnchor=\"middle\" fontSize={7} fill=\"currentColor\" opacity={0.85}>{col.notation}</text>}\n            {col.cells.map((cell) => {\n              const s = ringRollCellStyle(cell.mark, col.hollow)\n              const y = yOf(cell.lane)\n              return (\n                <g key={`cell${cell.offset}`}>\n                  <rect x={x0} y={r6(y + 2)} width={cw} height={LANE - 4} rx={2} fill={s.fill} fillOpacity={s.opacity} stroke=\"currentColor\" strokeOpacity={s.fill === \"none\" ? s.opacity : 0} strokeWidth={1.1} strokeDasharray={s.dash} />\n                  {cell.mark === \"bent\" && (\n                    <>\n                      <line x1={r6(x0 + cw - 5)} x2={r6(x0 + cw - 1)} y1={r6(y + 3)} y2={r6(y + LANE - 3)} stroke=\"currentColor\" strokeWidth={1.6} />\n                      {cw >= 20 && <text x={r6(x0 + 3)} y={r6(y + LANE / 2 + 3.5)} fontSize={9} fontWeight={700} fill=\"currentColor\">{cell.label}</text>}\n                    </>\n                  )}\n                </g>\n              )\n            })}\n            {col.wraps && col.segments.length === 2 && (\n              <>\n                <path d={`M ${r6(x0 + 4)} ${TOP - 1} Q ${mid} ${TOP - 7} ${r6(x0 + cw - 4)} ${TOP - 1}`} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.2} />\n                <path d={`M ${r6(x0 + 4)} ${bottom + 1} Q ${mid} ${bottom + 7} ${r6(x0 + cw - 4)} ${bottom + 1}`} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.2} />\n              </>\n            )}\n            {(layout === \"column\" || w >= 26) && <text x={mid} y={r6(bottom + 20)} textAnchor=\"middle\" fontSize={9} fill=\"currentColor\">{col.symbol ?? \"?\"}</text>}\n          </g>\n        )\n      })}\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/ring-roll.tsx"
    }
  ]
}
