{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-ring-circle",
  "type": "registry:ui",
  "title": "Ring Circle",
  "description": "A key's ring of chords as a circle of clickable nodes in line-of-thirds order, where neighbours share all but one note and a move of k nodes shares all but k. Edges carry the notes neighbours share, moves are drawn as arrows across the circle with the active one bold, and the hinge gets a heavier rim. Nodes arrive with their name, quality icon, ring label and pitch classes already chosen, so the same figure draws a traditional I to VII ring or any cycle of chords.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/ring-circle.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/ring-circle\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 Circle — a ring of chords drawn as a circle of clickable nodes.\n *\n * Built for the ring of a key: its seven chords in line-of-thirds order, so\n * neighbours share all but one note and a move of k nodes shares all but k.\n * Each edge between neighbours can carry the notes the two share; `moves` are\n * drawn as arrows across the circle, the active one bold; the `emphasis` node\n * (the hinge, where the line's two minor thirds meet) gets a heavier rim.\n * The nodes are drawn in the order given, so the same figure shows a\n * traditional I–VII ring, a moved ring, or any cycle of chords.\n *\n * View-only and structural: each node arrives with its name, icon, ring label\n * and pitch classes already chosen by the caller. The figure never analyses a\n * key. Pure SVG from props; nodes are keyboard-focusable buttons.\n *\n * Colours follow `currentColor`; the ground behind nodes (which knocks out the\n * lines under them) is `ground`, defaulting to the page background. The figure\n * fills its container's width; the caller caps it (`style`, or a class).\n */\n\nimport { useId, type CSSProperties } from \"react\"\nimport {\n  RING_CIRCLE,\n  ringCircleChord,\n  ringCircleEdgeLabel,\n  ringCirclePoint,\n  ringPcName,\n  ringSharedPcs,\n} from \"maruaudio/ui/shared/ring-geometry\"\n\nexport interface RingCircleNode {\n  /** Main label, usually the root's name (\"C\", \"F♯\"). */\n  readonly name: string\n  /** Quality glyph drawn before the name (☀ ☾ ◆ ✺). */\n  readonly icon?: string\n  /** Second line: the ring position or notation (\"−3\", \"−1⁴\"). */\n  readonly sub: string\n  /** Third, small line: a scale-degree caption (\"IV\"). */\n  readonly caption?: string\n  /** Pitch classes of the chord, for the shared-note labels on its edges. */\n  readonly pcs: readonly number[]\n  /** Heavier rim: the hinge. */\n  readonly emphasis?: boolean\n  /** Accessible name; defaults to \"name sub\". */\n  readonly ariaLabel?: string\n}\n\nexport interface RingCircleMove {\n  /** Indexes into `nodes`. */\n  readonly from: number\n  readonly to: number\n  readonly active?: boolean\n}\n\nexport interface RingCircleProps {\n  readonly nodes: readonly RingCircleNode[]\n  /** Index of the filled node, or null. */\n  readonly active: number | null\n  readonly onPick: (index: number) => void\n  /** Index drawn at twelve o'clock. */\n  readonly top?: number\n  readonly moves?: readonly RingCircleMove[]\n  /** Print the notes each pair of neighbours shares on their edge. */\n  readonly showShared?: boolean\n  /** Names the shared pitch classes; sharp spelling by default. */\n  readonly pcName?: (pc: number) => string\n  /** Fill behind nodes; defaults to the page background. */\n  readonly ground?: string\n  readonly className?: string\n  /** Sizing belongs to the caller: the figure fills its width (`width: 100%`); pass `maxWidth` here or by class. */\n  readonly style?: CSSProperties\n  readonly ariaLabel?: string\n}\n\nexport function RingCircle({\n  nodes,\n  active,\n  onPick,\n  top = 0,\n  moves = [],\n  showShared = true,\n  pcName = ringPcName,\n  ground = \"var(--background, #fff)\",\n  className,\n  style,\n  ariaLabel = \"A ring of chords drawn as a circle\",\n}: RingCircleProps) {\n  const arrowId = `ring-circle-arrow-${useId().replace(/[^A-Za-z0-9_-]/g, \"\")}`\n  const n = nodes.length\n  if (n === 0) return null\n  const r = RING_CIRCLE.node\n  const at = (i: number) => ringCirclePoint(i, n, top)\n  return (\n    <svg viewBox={`0 0 ${RING_CIRCLE.size} ${RING_CIRCLE.size}`} className={className} role=\"img\" aria-label={ariaLabel} style={{ display: \"block\", width: \"100%\", ...style }}>\n      <defs>\n        <marker id={arrowId} viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n          <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"currentColor\" />\n        </marker>\n      </defs>\n      {nodes.map((node, i) => {\n        const a = at(i), b = at((i + 1) % n)\n        const common = ringSharedPcs(node.pcs, nodes[(i + 1) % n]!.pcs)\n        const label = ringCircleEdgeLabel(a, b)\n        const lit = active !== null && (i === active || (i + 1) % n === active)\n        return (\n          <g key={`e${i}`}>\n            <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke=\"currentColor\" strokeWidth={lit ? 2.5 : 1} opacity={lit ? 0.8 : 0.3} />\n            {showShared && <text x={label.x} y={label.y} textAnchor=\"middle\" fontSize={7.5} fill=\"currentColor\" opacity={0.6}>{common.length ? common.map(pcName).join(\" \") : \"—\"}</text>}\n          </g>\n        )\n      })}\n      {moves.map((m, k) => {\n        if (m.from === m.to) return null\n        const c = ringCircleChord(at(m.from), at(m.to))\n        return <line key={`m${k}`} {...c} stroke=\"currentColor\" strokeWidth={m.active ? 3 : 1.2} opacity={m.active ? 0.95 : 0.3} markerEnd={`url(#${arrowId})`} />\n      })}\n      {nodes.map((node, i) => {\n        const p = at(i)\n        const isActive = active === i\n        const ink = isActive ? ground : \"currentColor\"\n        const pick = () => onPick(i)\n        return (\n          <g\n            key={`n${i}`}\n            onClick={pick}\n            role=\"button\"\n            tabIndex={0}\n            onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); pick() } }}\n            style={{ cursor: \"pointer\" }}\n            aria-label={node.ariaLabel ?? `${node.name} ${node.sub}`}\n          >\n            <circle cx={p.x} cy={p.y} r={r} fill={isActive ? \"currentColor\" : ground} stroke=\"currentColor\" strokeWidth={node.emphasis ? 2.6 : 1.4} />\n            <text x={p.x} y={p.y - 3} textAnchor=\"middle\" fontSize={11} fontWeight={700} fill={ink}>{node.icon ? `${node.icon} ${node.name}` : node.name}</text>\n            <text x={p.x} y={p.y + 8} textAnchor=\"middle\" fontSize={8} fill={ink} opacity={0.9}>{node.sub}</text>\n            {node.caption && <text x={p.x} y={p.y + 16} textAnchor=\"middle\" fontSize={6} fill={ink} opacity={0.6}>{node.caption}</text>}\n          </g>\n        )\n      })}\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/ring-circle.tsx"
    }
  ]
}
