{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-harmony-lattice",
  "type": "registry:ui",
  "title": "Harmony Lattice",
  "description": "A twelve-bin field painted onto an isomorphic pad layout: two semitone steps on a square or hexagonal grid, the piano or a fretboard for comparison. Paints by value (the kit's colour ramps, or a tense/bright ramp) or by pitch identity (hue by semitone or by circle of fifths), and rings the notes a progression step adds and dashes the ones it releases, so a chord sequence can be animated on several layouts at once. Every cell is coloured by the value of the pitch class it sounds, so a chord's consonant neighbourhood, a song's note histogram or a scale mask can be drawn on several layouts side by side and read as regions or as scatter. Chord tones take the accent, tense values are tinted rather than merely dark, a tonic gets a ring and a tonal-centre marker can sit over the lattice. A display envelope and a live trail beside it keep played pads glowing and fading, so recent chords leave a decaying trail. The pure geometry beside it also scores a field's readability on a layout: roughness across neighbours and the number of connected regions.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/harmony-lattice.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/harmony-lattice\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 * Harmony Lattice — a twelve-bin field painted onto an isomorphic pad layout.\n *\n * Give it a layout (two semitone steps on a square or hexagonal grid, the\n * piano, or a fretboard) and a field (one 0..1 value per pitch class) and it\n * paints every cell by the value of the pitch class it sounds. The same field\n * drawn on several layouts side by side is the whole point: a chord's\n * consonant neighbourhood, a song's note histogram or a scale mask reads as\n * regions on one layout and as scatter on another, and this is how you see\n * which.\n *\n * Two ways to paint:\n *\n *   paint=\"field\"  the value drives the colour. With no `colorScale` the pads\n *                  run from dim (0) to bright (1), values under `tense` tinted\n *                  so a dissonant pad is a colour and not just an unlit one;\n *                  with a named scale (`viridis`, `magma`, …) the kit's shared\n *                  ramp is used instead.\n *   paint=\"midi\"   the pad's actual pitch drives the colour on the ramp, low to\n *                  high over `pitchRange` (default MIDI 36..96), so the\n *                  direction pitch rises in on a layout is a visible gradient.\n *   paint=\"pitch\"  the pitch class drives the hue, on the `chromatic` wheel\n *                  (hue by semitone) or the `fifths` wheel (hue by circle of\n *                  fifths), measured from `tonicPc`. The field, if any, only\n *                  dims. Whichever wheel runs smoothly across a layout's pads\n *                  is the relationship that layout makes visible.\n *\n * Chord tones take the accent colour. For animating a progression, `addedPcs`\n * get a bright ring (new this step) and `releasedPcs` a dashed outline (just\n * left), so a step reads as what stayed, what came and what went. The same\n * three states can be given per PAD with `cellMarks` (from\n * `latticeStepMarks`): real notes on particular pads, so a chord is one shape\n * on the surface and not the same shape in every octave. A `noteField`\n * (indexed by MIDI note, with `base` naming the bottom-left pad's note) paints\n * each pad by the note it sounds rather than its pitch class, which is what a\n * roughness model of real partials produces. `cellGlow` is a player's layer:\n * a light overlay per pad whose opacity is the note's activity, so sounding\n * notes light up and fade (the envelope and a live trail are in\n * `harmony-lattice-trail.ts`). `lineCells` (from `latticeThirdsLine`) draws the\n * key's line of thirds as light links between consecutive pads, so the\n * staircase a progression walks on is visible under the chords. A `tonicPc`\n * gets a ring, and a `marker` (a point in cell units, for a tonal centre of\n * gravity) is drawn over the lattice.\n *\n * `presentation` chooses how much of the window is shown: \"full\" (every pad),\n * \"band\" (each note once: the pads that repeat a note already nearer the\n * window's diagonal are dimmed, two corner triangles on Aura) or \"sheared\"\n * (the band alone, rows slid so the line of thirds stands upright: on Aura\n * three pads right per four up is two octaves, so the shear is −0.75 per row;\n * in general −vertical/horizontal, skipped when steeper than 1.5).\n *\n * Pure SVG from props, no measurement, no effects. The geometry and the two\n * readability measures live in `harmony-lattice-geometry.ts`.\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\"\nimport {\n  COLOR_SCALES,\n  colorLuminance,\n  pitchWheelColor,\n  type ColorScaleName,\n  type PitchWheelName,\n} from \"maruaudio/ui/shared/color-scale\"\nimport {\n  latticeBand,\n  latticeDrawing,\n  latticeFieldColour,\n  latticeMidi,\n  latticeMod12,\n  type LatticeCellMark,\n  type LatticeLayout,\n} from \"maruaudio/ui/shared/harmony-lattice-geometry\"\n\nexport interface HarmonyLatticeMarker {\n  /** Position in cell units from the bottom-left cell's centre (square/hex/strings) or key index (piano). */\n  readonly col: number\n  readonly row: number\n  /** 0..1, drives the marker's size and opacity. */\n  readonly strength?: number\n  readonly label?: string\n}\n\nexport type HarmonyLatticePaint = \"field\" | \"pitch\" | \"midi\"\n\nexport type HarmonyLatticePresentation = \"full\" | \"band\" | \"sheared\"\n\nexport interface HarmonyLatticeProps {\n  layout: LatticeLayout\n  /** Twelve values, chromatic (0 = C), each 0..1. */\n  field: ReadonlyArray<number>\n  /** Cells per side of the window. Defaults to 8. Ignored on the piano and on strings. */\n  size?: number\n  /** Pitch classes painted as chord tones. */\n  chordPcs?: ReadonlyArray<number>\n  /** Chord tones new at this step: a bright ring. */\n  addedPcs?: ReadonlyArray<number>\n  /** Pitch classes just released: a dashed outline. */\n  releasedPcs?: ReadonlyArray<number>\n  /** Per-pad states for real notes (see `latticeStepMarks`). */\n  cellMarks?: ReadonlyArray<LatticeCellMark>\n  /**\n   * Mark every pad that sounds the same pitch class as a held or added pad but\n   * is not that pad: a thin accent ring with a centre dot, so an octave of a\n   * held note reads as \"same note, other octave\" rather than as a stranger.\n   */\n  markOctaves?: boolean\n  /** Per-pad activity 0..1 for a player: a light overlay that fades with the level. */\n  cellGlow?: ReadonlyArray<{ readonly col: number; readonly row: number; readonly level: number }>\n  /** How much of the window to show. Defaults to \"full\". */\n  presentation?: HarmonyLatticePresentation\n  /** The key's line of thirds: consecutive pads are linked, and each gets a faint inset. */\n  lineCells?: ReadonlyArray<{ readonly col: number; readonly row: number }>\n  /** A value per MIDI note (index = note), used instead of `field` when given. */\n  noteField?: ReadonlyArray<number>\n  /** MIDI note of the bottom-left pad (lattices and piano). Defaults to 48. */\n  base?: number\n  /** A pitch class to ring. */\n  tonicPc?: number\n  /** What the colour encodes. Defaults to \"field\". */\n  paint?: HarmonyLatticePaint\n  /** Field paint: a named ramp from the kit's colour scales. Omit for the tense/bright ramp. */\n  colorScale?: ColorScaleName\n  /** Pitch paint: which wheel. Defaults to \"fifths\". */\n  wheel?: PitchWheelName\n  /** Pitch paint: dim a pad by its field value. Defaults to false. */\n  dimByField?: boolean\n  /** Midi paint: the MIDI range mapped onto the ramp. Defaults to [36, 96]. */\n  pitchRange?: readonly [number, number]\n  /** Below this value a pad is tinted as tense (tense/bright ramp only). Defaults to 0.35. */\n  tense?: number\n  /** Pad pitch in viewBox units. Defaults to 32. */\n  cell?: number\n  /** Draw names on the pads. Defaults to true. */\n  showLabels?: boolean\n  /** What the label says: the pitch class (\"C\"), the note with its octave (\"C4\"), or the MIDI number (\"60\"). Defaults to \"pc\". */\n  labelStyle?: \"pc\" | \"note\" | \"midi\"\n  /** Spell labels with flats. */\n  flats?: boolean\n  marker?: HarmonyLatticeMarker | null\n  /**\n   * Called when a pad is clicked, with the pad and the note it sounds. Pads get\n   * a pointer cursor and a hover highlight whenever this is set.\n   */\n  onCellClick?: (cell: { col: number; row: number; midi: number; pitchClass: number }) => void\n  /** Chord-tone colour. Defaults to the accent token. */\n  color?: string\n  width?: number | string\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nconst LIGHT = \"#f4f4f6\"\nconst DARK = \"#15171c\"\nconst ADDED = \"#ffd54a\"\n\nexport function HarmonyLattice({\n  layout,\n  field,\n  size = 8,\n  chordPcs,\n  addedPcs,\n  releasedPcs,\n  cellMarks,\n  markOctaves = false,\n  cellGlow,\n  lineCells,\n  presentation = \"full\",\n  noteField,\n  base = 48,\n  tonicPc,\n  paint = \"field\",\n  colorScale,\n  wheel = \"fifths\",\n  dimByField = false,\n  pitchRange = [36, 96],\n  tense = 0.35,\n  cell = 32,\n  showLabels = true,\n  labelStyle = \"pc\",\n  flats = false,\n  marker = null,\n  onCellClick,\n  color,\n  width = \"100%\",\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: HarmonyLatticeProps) {\n  const accent = color ?? token.accent\n  const band = presentation === \"full\" ? null : latticeBand(layout, size, base)\n  const hiddenKeys = new Set((band?.hidden ?? []).map((c) => `${c.col},${c.row}`))\n  // The band runs along the direction where the two steps add up (vertical/horizontal: Aura's three right\n  // per four up), so sliding each row by −vertical/horizontal makes it upright. Steeper ratios than 1.5\n  // would smear the window; those stay unsheared and only drop the doubles.\n  const ratio = layout.geometry === \"square\" && layout.horizontal > 0 ? layout.vertical / layout.horizontal : 0\n  const shear = presentation === \"sheared\" && ratio > 0 && ratio <= 1.5 ? -ratio : 0\n  const drawing = latticeDrawing(layout, size, cell, base, { shear, omit: presentation === \"sheared\" ? hiddenKeys : undefined })\n  const chord = new Set((chordPcs ?? []).map(latticeMod12))\n  const added = new Set((addedPcs ?? []).map(latticeMod12))\n  const released = new Set((releasedPcs ?? []).map(latticeMod12))\n  const tonic = tonicPc === undefined ? undefined : latticeMod12(tonicPc)\n  const fontSize = Math.max(6, cell * (labelStyle === \"pc\" ? 0.28 : 0.22))\n  const labelFor = (pc: number, midi: number): string => {\n    const name = pitchClassName(pc, flats ? \"unicode-flat\" : \"unicode-sharp\")\n    if (labelStyle === \"midi\") return String(midi)\n    if (labelStyle === \"note\") return `${name}${Math.floor(midi / 12) - 1}`\n    return name\n  }\n  const ramp = colorScale ? COLOR_SCALES[colorScale] : undefined\n\n  const markAt = new Map<string, LatticeCellMark[\"state\"]>()\n  for (const m of cellMarks ?? []) markAt.set(`${m.col},${m.row}`, m.state)\n  const heldPcs = new Set<number>()\n  if (markOctaves) for (const m of cellMarks ?? []) if (m.state !== \"released\") heldPcs.add(latticeMod12(m.midi))\n  const onLine = new Set((lineCells ?? []).map((c) => `${c.col},${c.row}`))\n  const glowAt = new Map<string, number>()\n  for (const g of cellGlow ?? []) if (g.level > 0.01) glowAt.set(`${g.col},${g.row}`, Math.min(1, g.level))\n\n  const fillFor = (pc: number, isTone: boolean, midi: number): { fill: string; dark: boolean } => {\n    const value = noteField ? (noteField[midi] ?? 0) : (field[pc] ?? 0)\n    // A held note is a white pad with a dark name: legible on every ramp and every wheel.\n    if (isTone) return { fill: LIGHT, dark: true }\n    if (paint === \"midi\") {\n      const t = Math.min(1, Math.max(0, (midi - pitchRange[0]) / Math.max(1, pitchRange[1] - pitchRange[0])))\n      const rgb = (ramp ?? COLOR_SCALES.viridis)(t)\n      return { fill: `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`, dark: colorLuminance(rgb) > 0.5 }\n    }\n    if (paint === \"pitch\") {\n      const rgb = pitchWheelColor(pc, wheel, tonic ?? 0, dimByField ? value : 1)\n      return { fill: `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`, dark: colorLuminance(rgb) > 0.5 }\n    }\n    if (ramp) {\n      const rgb = ramp(value)\n      return { fill: `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`, dark: colorLuminance(rgb) > 0.5 }\n    }\n    return { fill: latticeFieldColour(value, false, accent, tense), dark: value >= 0.5 }\n  }\n\n  // Six decimals, so server rendering and browser hydration agree despite platform rounding.\n  const six = (v: number) => Number(v.toFixed(6))\n  const rawMarker = marker\n    ? layout.geometry === \"piano\"\n      ? { x: (marker.col + 0.5) * cell * 0.9, y: drawing.height * 0.35 }\n      : layout.geometry === \"hex\"\n        ? { x: (Math.sqrt(3) * cell) / 4 + (marker.col + marker.row / 2) * Math.sqrt(3) * (cell / 2), y: drawing.height - cell / 2 - marker.row * 0.75 * cell }\n        : { x: (shear < 0 ? -shear * (size - 1) * cell : 0) + (marker.col + marker.row * shear + 0.5) * cell - 1, y: drawing.height - (marker.row + 0.5) * cell - 1 }\n    : null\n  const markerPoint = rawMarker ? { x: six(rawMarker.x), y: six(rawMarker.y) } : null\n  // Links between consecutive pads of the line, trimmed so they do not cross the labels.\n  const centreOf = new Map(drawing.shapes.map((s) => [`${s.cell.col},${s.cell.row}`, { x: s.cx, y: s.cy }]))\n  const lineLinks: Array<{ x1: number; y1: number; x2: number; y2: number }> = []\n  for (let i = 1; i < (lineCells?.length ?? 0); i++) {\n    const a = centreOf.get(`${lineCells![i - 1]!.col},${lineCells![i - 1]!.row}`)\n    const b = centreOf.get(`${lineCells![i]!.col},${lineCells![i]!.row}`)\n    if (!a || !b) continue\n    const dx = b.x - a.x, dy = b.y - a.y\n    const len = Math.hypot(dx, dy) || 1\n    const trim = Math.min(len * 0.45, cell * 0.3)\n    lineLinks.push({ x1: six(a.x + (dx / len) * trim), y1: six(a.y + (dy / len) * trim), x2: six(b.x - (dx / len) * trim), y2: six(b.y - (dy / len) * trim) })\n  }\n  return (\n    <svg\n      viewBox={`0 0 ${drawing.width} ${drawing.height}`}\n      width={width}\n      role=\"img\"\n      aria-label={ariaLabel ?? `${layout.label ?? layout.geometry} lattice`}\n      className={className}\n      style={{ display: \"block\", fontFamily: token.font, ...style }}\n    >\n      {/* Hover lift on every pad; a pointer only when there is something to click. */}\n      <style>{`.hl-pad { transition: filter 120ms ease, opacity 120ms ease; } .hl-pad:hover > rect:first-child, .hl-pad:hover > polygon:first-child { filter: brightness(1.25); stroke: ${LIGHT}; stroke-width: 1.6; }${onCellClick ? \" .hl-pad { cursor: pointer; }\" : \"\"}`}</style>\n      {drawing.shapes.map((s) => {\n        const pc = s.cell.pitchClass\n        const key = `${s.cell.col},${s.cell.row}`\n        const mark = markAt.get(key)\n        const isTone = chord.has(pc) || mark === \"held\" || mark === \"added\"\n        const { fill, dark: darkFill } = fillFor(pc, isTone, latticeMidi(layout, s.cell.col, s.cell.row, base))\n        const glow = glowAt.get(key) ?? 0\n        const dark = darkFill || glow > 0.5\n        const labelFill = dark ? DARK : LIGHT\n        const inset = (stroke: string, strokeWidth: number, dashed = false, opacity = 1) =>\n          s.kind === \"polygon\"\n            ? <circle cx={s.cx} cy={s.cy} r={cell * 0.36} fill=\"none\" stroke={stroke} strokeWidth={strokeWidth} strokeDasharray={dashed ? \"3 2\" : undefined} opacity={opacity} />\n            : <rect x={s.x + 2.5} y={s.y + 2.5} width={s.width - 5} height={s.height - 5} rx={3} fill=\"none\" stroke={stroke} strokeWidth={strokeWidth} strokeDasharray={dashed ? \"3 2\" : undefined} opacity={opacity} />\n        const midi = latticeMidi(layout, s.cell.col, s.cell.row, base)\n        if (hiddenKeys.has(key)) {\n          // A doubled note: kept as a dim outline so the window's shape stays readable, nothing on it.\n          return (\n            <g key={key} opacity={0.22} pointerEvents=\"none\" aria-hidden=\"true\">\n              {s.kind === \"polygon\"\n                ? <polygon points={s.points} fill=\"none\" stroke={token.border} strokeWidth={0.8} strokeDasharray=\"2 2\" />\n                : <rect x={s.x} y={s.y} width={s.width} height={s.height} rx={4} fill=\"none\" stroke={token.border} strokeWidth={0.8} strokeDasharray=\"2 2\" />}\n            </g>\n          )\n        }\n        return (\n          <g\n            key={key}\n            className=\"hl-pad\"\n            onClick={onCellClick ? () => onCellClick({ col: s.cell.col, row: s.cell.row, midi, pitchClass: pc }) : undefined}\n            role={onCellClick ? \"button\" : undefined}\n            tabIndex={onCellClick ? 0 : undefined}\n            onKeyDown={onCellClick ? (e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onCellClick({ col: s.cell.col, row: s.cell.row, midi, pitchClass: pc }) } } : undefined}\n            aria-label={onCellClick ? `${pitchClassName(pc, flats ? \"unicode-flat\" : \"unicode-sharp\")} (MIDI ${midi})` : undefined}\n          >\n            {s.kind === \"polygon\" ? (\n              <polygon points={s.points} fill={fill} stroke={token.border} strokeWidth={0.8} />\n            ) : (\n              <rect x={s.x} y={s.y} width={s.width} height={s.height} rx={s.raised ? 2 : layout.geometry === \"piano\" ? 2 : 4} fill={fill} stroke={token.border} strokeWidth={0.8} />\n            )}\n            {glow > 0 && (\n              // A dark halo under the yellow edge keeps a lit pad visible on a bright field too.\n              s.kind === \"polygon\"\n                ? <><polygon points={s.points} fill=\"none\" opacity={0.7 * glow} stroke={DARK} strokeWidth={4 * glow} /><polygon points={s.points} fill={LIGHT} opacity={0.85 * glow} stroke={ADDED} strokeWidth={2 * glow} /></>\n                : <><rect x={s.x} y={s.y} width={s.width} height={s.height} rx={4} fill=\"none\" opacity={0.7 * glow} stroke={DARK} strokeWidth={4 * glow} /><rect x={s.x} y={s.y} width={s.width} height={s.height} rx={4} fill={LIGHT} opacity={0.85 * glow} stroke={ADDED} strokeWidth={2 * glow} /></>\n            )}\n            {onLine.has(key) && !isTone && inset(LIGHT, 1, false, 0.55)}\n            {markOctaves && !isTone && heldPcs.has(pc) && (\n              <>\n                {inset(accent, 1.4)}\n                <circle cx={s.cx} cy={s.cy - fontSize * 0.9} r={Math.max(1.5, cell * 0.07)} fill={accent} />\n              </>\n            )}\n            {tonic === pc && !isTone && inset(LIGHT, 1.6)}\n            {isTone && inset(added.has(pc) || mark === \"added\" ? ADDED : accent, 3)}\n            {!isTone && added.has(pc) && inset(ADDED, 2.4)}\n            {((released.has(pc) && !isTone) || mark === \"released\") && inset(LIGHT, 1.2, true)}\n            {showLabels && (\n              <text x={s.cx} y={s.cy + fontSize * 0.36} textAnchor=\"middle\" fontSize={fontSize} fill={labelFill}>\n                {labelFor(pc, midi)}\n              </text>\n            )}\n          </g>\n        )\n      })}\n      {lineLinks.length > 0 && (\n        <g pointerEvents=\"none\" strokeLinecap=\"round\">\n          {lineLinks.map((l, i) => (\n            <g key={i}>\n              <line x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2} stroke={DARK} strokeWidth={Math.max(2, cell * 0.16)} opacity={0.6} />\n              <line x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2} stroke={LIGHT} strokeWidth={Math.max(1, cell * 0.08)} opacity={0.9} />\n            </g>\n          ))}\n        </g>\n      )}\n      {markerPoint && marker && (\n        <g opacity={0.35 + 0.65 * Math.min(1, Math.max(0, marker.strength ?? 1))}>\n          <circle cx={markerPoint.x} cy={markerPoint.y} r={cell * (0.18 + 0.22 * Math.min(1, Math.max(0, marker.strength ?? 1)))} fill=\"#ffffff\" stroke={DARK} strokeWidth={1.5} />\n          {marker.label && (\n            <text x={markerPoint.x} y={markerPoint.y - cell * 0.5} textAnchor=\"middle\" fontSize={fontSize} fill={LIGHT} stroke={DARK} strokeWidth={2} paintOrder=\"stroke\">\n              {marker.label}\n            </text>\n          )}\n        </g>\n      )}\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/harmony-lattice.tsx"
    },
    {
      "path": "components/ui/harmony-lattice-projections.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/harmony-lattice\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 { useMemo } from \"react\"\nimport { pitchClassName } from \"maruaudio/ui/shared/music-theory-pitch-class\"\nimport { useElementSize } from \"maruaudio/ui/shared/use-element-size\"\nimport { PROJECTION_CHAIN, PROJECTION_LAYOUTS, projectionDetail, projectionKey, projectionScene, type ProjectionLayout, type ProjectionNote, type ProjectionPoint, type ProjectionShape, type ProjectionContext } from \"maruaudio/ui/shared/harmony-lattice-projections-model\"\nimport \"@/components/ui/harmony-lattice-projections.css\"\n\nconst points = (ps: readonly ProjectionPoint[]) => ps.map(p=>`${p.x.toFixed(4)},${p.y.toFixed(4)}`).join(\" \")\nconst path = (ps: readonly ProjectionPoint[]) => ps.map((p,i)=>`${i?'L':'M'}${p.x.toFixed(4)},${p.y.toFixed(4)}`).join(\" \")\nconst name = (note: ProjectionNote) => `${pitchClassName(note.midi % 12, \"unicode-sharp\")}${Math.floor(note.midi / 12)-1}`\n\n/** Controlled projection renderer. No music clock, notes, device access or progression state. */\nexport function HarmonyLatticeProjections({ layout, current, previous = [], context, count = 15, shape = \"chain\", lift = 0, ariaLabel }: {\n  layout: ProjectionLayout\n  current: readonly ProjectionNote[]\n  previous?: readonly ProjectionNote[]\n  context?: ProjectionContext\n  count?: number\n  shape?: ProjectionShape\n  lift?: number\n  ariaLabel?: string\n}) {\n  const [ref,size] = useElementSize<HTMLDivElement>()\n  const width = Math.max(320,size.width || 680)\n  const scene = useMemo(()=>projectionScene(layout,count,shape,current,previous,width,lift,context),[layout,count,shape,current,previous,width,lift,context])\n  const now=new Set(current.map(projectionKey)),before=new Set(previous.map(projectionKey)),chain=new Set((context?.chain ?? PROJECTION_CHAIN).slice(0,count).map(projectionKey))\n  const mode=PROJECTION_LAYOUTS.find(p=>p.id===layout)!\n  const isRing=layout==='ring'||layout==='equal-ring'\n  return <div ref={ref} className=\"harmony-projections\">\n      <svg viewBox={`0 0 ${scene.width} ${scene.height}`} role=\"img\" aria-label={ariaLabel ?? mode.label}>\n        <desc>{mode.label}</desc><desc>{`${projectionDetail(layout,context)} Selected notes: ${current.map(name).join(', ')}.`}</desc>\n        {scene.cells.map(c=>{\n          const key=projectionKey(c),state=now.has(key)?before.has(key)?'held':'added':before.has(key)?'released':chain.has(key)?'path':'empty'\n          return <g key={c.id} data-cell={key} data-midi={c.midi} data-state={state}>\n            <desc>{`${name(c)} · square (${c.col},${c.row}) · ${state}`}</desc>\n            <polygon className={`hp-cell hp-cell-${state}`} points={points(c.polygon)} />\n          </g>\n        })}\n        {scene.links.map((l,i)=><path key={i} className={`hp-link hp-link-${l.interval}`} d={path(l.points)} />)}\n        {scene.contours.map(c=><g key={c.label}><path className=\"hp-contour\" d={path(c.points)} /><text x={c.at.x} y={c.at.y+4} textAnchor=\"middle\">{c.label}</text></g>)}\n        {scene.links.filter(l=>l.label).map((l,i)=>{const p=l.points[Math.floor(l.points.length/2)]!;return <text key={i} x={p.x} y={p.y-10} textAnchor=\"middle\">{l.label}</text>})}\n        {scene.cells.map(c=>{\n          const key=projectionKey(c),side=Math.hypot(c.polygon[1]!.x-c.polygon[0]!.x,c.polygon[1]!.y-c.polygon[0]!.y)\n          const labels=layout==='polar'?now.has(key)||before.has(key):isRing||side>=32||chain.has(key)||now.has(key)\n          return <g key={c.id}>{labels&&<text x={c.center.x} y={c.center.y+4} textAnchor=\"middle\">{name(c)}</text>}{now.has(key)&&before.has(key)&&<circle className=\"hp-kept\" cx={c.center.x+Math.min(12,side*.3)} cy={c.center.y-Math.min(12,side*.3)} r={2.5} />}</g>\n        })}\n        {scene.junctions.map((j,i)=><g key={i} data-junction={j.kind}><desc>{j.kind==='dim'?'Middle of +3/+3 (diminished)':'Middle of +4/+4 (augmented)'}</desc>{j.kind==='dim'?<polygon className=\"hp-junction\" points={`${j.point.x},${j.point.y-4} ${j.point.x+4},${j.point.y} ${j.point.x},${j.point.y+4} ${j.point.x-4},${j.point.y}`} />:<rect className=\"hp-junction\" x={j.point.x-6} y={j.point.y-2} width={12} height={4} />}</g>)}\n      </svg>\n  </div>\n}\n",
      "type": "registry:component",
      "target": "@ui/harmony-lattice-projections.tsx"
    },
    {
      "path": "components/ui/harmony-lattice-projections.css",
      "content": "/**\n * Source: https://maru.audio/ui-kit/harmony-lattice\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.harmony-projections {\n  --hp-text: var(--foreground, var(--uikit-text, #e3e8ec));\n  --hp-surface: var(--background, var(--uikit-surface, #141a1f));\n  --hp-border: var(--border, var(--uikit-border, #3a4652));\n  --hp-muted: var(--muted, var(--uikit-cap, #25303a));\n  --hp-accent: var(--primary, var(--uikit-accent, #e8c574));\n  width: 100%; max-width: 960px;\n}\n.harmony-projections svg { display: block; width: 100%; }\n.harmony-projections svg text { fill: var(--hp-text); font-size: 12px; paint-order: stroke; stroke: var(--hp-surface); stroke-width: 3px; }\n.harmony-projections .hp-cell { fill: color-mix(in srgb, var(--hp-muted) 55%, transparent); stroke: var(--hp-border); stroke-width: 1; }\n.harmony-projections .hp-cell-path { fill: color-mix(in srgb, var(--hp-text) 6%, var(--hp-surface)); }\n.harmony-projections .hp-cell-added { fill: color-mix(in srgb, var(--hp-accent) 30%, var(--hp-surface)); stroke: var(--hp-text); }\n.harmony-projections .hp-cell-held { fill: color-mix(in srgb, var(--hp-accent) 20%, var(--hp-surface)); stroke: var(--hp-text); }\n.harmony-projections .hp-cell-released { fill: var(--hp-surface); stroke: var(--hp-text); stroke-dasharray: 3 3; }\n.harmony-projections .hp-link { fill: none; stroke: var(--hp-text); stroke-width: 2.5; pointer-events: none; }\n.harmony-projections .hp-link-4 { stroke: var(--chart-1, var(--hp-text)); }\n.harmony-projections .hp-link-3 { stroke: var(--chart-2, var(--hp-text)); stroke-dasharray: 5 3; }\n.harmony-projections .hp-link-0, .harmony-projections .hp-contour { fill: none; stroke: var(--hp-border); stroke-width: 1; stroke-dasharray: 4 4; }\n.harmony-projections .hp-junction { fill: var(--hp-text); stroke: var(--hp-surface); stroke-width: 1; }\n.harmony-projections .hp-kept { fill: var(--hp-text); }\n\n.harmony-projections .hp-cell { transition: fill .18s, stroke .18s; }\n@media (prefers-reduced-motion: reduce) { .harmony-projections .hp-cell { transition: none; } }\n",
      "type": "registry:file",
      "target": "@ui/harmony-lattice-projections.css"
    }
  ]
}
