{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-beat-grid-scope",
  "type": "registry:ui",
  "title": "Beat Grid Scope",
  "description": "A dynamics scope's chrome: a bars-across grid with bar lines heavier than beats and an optional sixteenth grid, a dB gutter reading downward from 0, a dashed hard-ceiling line with its own label (dashed because a budget is a limit, not a measurement), a playhead, and beneath the plot a strip of event lanes drawn as onset ticks at their exact beat — so a swung event visibly sits off its gridline — sharing the plot's own xForBeat, with the playhead continuing through them. It draws no signal itself: `children` is a render function handed the resolved layout, so a prediction envelope, a measured overlay and a waveform all live in the SAME coordinate space as the grid and cannot disagree about where a beat is. The playhead is either given as a phase (null draws none, which is the right picture for a stopped transport) or derived from a host transport through useBeatClock, whose source is reported back so the caption can distinguish host-locked from free-running. SVG, no measurement, renders on the server.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/beat-grid-scope.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/beat-grid-scope\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 * BeatGridScope — a bars-across, dB-down plot surface with an event-lane strip\n * beneath it, and a render slot where the consumer draws its own signal.\n *\n * ## What it is, and what it deliberately is not\n *\n * This is the CHROME of a dynamics scope: the bar/beat/sixteenth grid, the dB\n * gutter, the hard ceiling line, the playhead, and — below the plot — a strip\n * of event lanes sharing the same x axis. It draws no signal of its own. The\n * curve, the waveform, the measured overlay all come from `children`, which is\n * a render function handed the resolved layout:\n *\n *     <BeatGridScope bars={2} ceilingDb={14} lanes={lanes}>\n *       {(layout) => <path d={envelopePath(layout)} stroke=\"…\" fill=\"none\" />}\n *     </BeatGridScope>\n *\n * That is the whole point of the layer slot: the consumer's paths are in the\n * SAME coordinate space as the grid they sit on, so a curve, a waveform and an\n * onset tick cannot disagree about where beat 3 is. There is no second axis to\n * keep in sync, because there is no second axis.\n *\n * ## Honesty about time\n *\n * The playhead is either given (`phase`), or derived from a host transport /\n * bpm through `useBeatClock` — and in the derived case the clock's `source`\n * comes back out through `onClockChange` and lands in the caption, so a face\n * can say \"host-locked\" or \"free-running at 128\" rather than implying a sync it\n * does not have. `phase={null}` draws no playhead at all, which is the right\n * picture for a stopped transport: nothing pinned at zero.\n *\n * The ceiling is drawn as a dashed line with its own label because it is a\n * BUDGET, not a measurement — a dashed line reads as a limit, a solid one\n * reads as data.\n *\n * ## Honesty about depth\n *\n * The dB gutter's bottom label is the depth the axis ACTUALLY reaches, not a\n * decorative \"-∞\". A face that scales `axisDb` to live data cannot know in\n * advance how deep its plot ended up, and a fixed infinity symbol on a three-\n * decibel axis is a plain falsehood. `axisFloorLabel` overrides it for a plot\n * whose floor really is silence. The steps between are snapped to round dB\n * values (1 / 2 / 3 / 6 / 12) so the numbers read as a fader's own detents\n * rather than as an arbitrary division of whatever the axis happened to be.\n *\n * ## The event lanes\n *\n * Lanes are onset ticks at their exact beat, on the plot's own `xForBeat` — so\n * a swung hit visibly sits off its gridline, the same call `drum-lane` makes\n * and for the same reason. `strength` sets a tick's height and opacity so a\n * ghost reads as a ghost; `ghost: true` draws it hollow; `recoveryBeat` draws a\n * tail from the onset to where the recovery completes. A lane may also carry a\n * one-glyph `marker` drawn in the gutter beside its name, so the strip itself\n * points at the lane a readout elsewhere is talking about. The playhead\n * continues down through the lanes, because an onset and the curve that\n * answered it must be readable against one cursor.\n *\n * ## Rendering\n *\n * SVG with a fixed layout box scaled to the element's width — no measurement,\n * no canvas, no `window` outside `useBeatClock`'s effect. The component renders\n * identically on the server, which is what lets a module face be server-rendered\n * and a face-render test assert on the markup. Test hooks: `data-beat-grid-scope`\n * on the root, `data-beat-grid-lane` (with `data-lane-id`) per lane,\n * `data-beat-grid-playhead`, `data-beat-grid-ceiling`.\n */\n\nimport { useEffect, useId } from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  createBeatGridLayout,\n  type BeatGridDbTickOptions,\n  type BeatGridLayout,\n} from \"maruaudio/ui/shared/beat-grid-geometry\"\nimport {\n  useBeatClock,\n  type BeatClockSource,\n  type BeatClockTransport,\n} from \"maruaudio/ui/shared/beat-clock\"\n\n/** One onset on a lane. Placed by beat — its exact value is what shows swing. */\nexport interface BeatGridEvent {\n  /** Onset in beats from the window's start. */\n  readonly beat: number\n  /** 0..1. Tick height and opacity, so a ghost reads as a ghost. Default 1. */\n  readonly strength?: number\n  /** Draw hollow — a predicted or inferred onset rather than a detected one. */\n  readonly ghost?: boolean\n  /**\n   * Beat at which this event's recovery completes, if the lane models one.\n   * Drawn as a tail from the onset, so \"how long this hit held the gate open\"\n   * is legible next to the curve above it.\n   */\n  readonly recoveryBeat?: number\n}\n\n/** One lane in the strip beneath the plot. */\nexport interface BeatGridLane {\n  /** Stable id — also the `data-lane-id` a test matches on. */\n  readonly id: string\n  /** Gutter caption. */\n  readonly label: string\n  /** Per-lane accent. Falls back to the scope's accent. */\n  readonly color?: string\n  /**\n   * A one-glyph mark drawn in the gutter before the label — the place for a\n   * \"this is the lane that matters\" pointer, so the strip itself says which\n   * lane a readout elsewhere is talking about. Drawn in the lane's own colour\n   * and hidden from assistive technology, which reads the label instead.\n   */\n  readonly marker?: string\n  /** The onsets. */\n  readonly events: readonly BeatGridEvent[]\n}\n\nexport interface BeatGridScopeProps {\n  /** Bars across the window. Defaults to 2. */\n  bars?: number\n  /** Quarter-note beats per bar. Defaults to 4. */\n  beatsPerBar?: number\n  /** Grid lines per beat. 0 or 1 draws no sixteenth grid. Defaults to 4. */\n  subdivisions?: number\n\n  /** How far down the dB axis reaches, in dB of reduction. Defaults to 24. */\n  axisDb?: number\n  /** Floor on `axisDb`, so a quiet passage is not magnified. Defaults to 6. */\n  minAxisDb?: number\n  /** Number of dB gutter labels below 0. Defaults to 4. */\n  dbTickCount?: number\n  /**\n   * What the bottom of the dB gutter prints.\n   *\n   * The default is the depth the axis ACTUALLY reaches — `\"-3.0\"` on a 3 dB\n   * axis. That matters because a caller scaling `axisDb` to live data does not\n   * know in advance how deep the plot ended up: a fixed `\"-∞\"` would claim\n   * silence on an axis three decibels deep, which is simply untrue. Pass\n   * `\"-∞\"` when the plot's floor really is silence, or `null` for a floor rule\n   * with no label.\n   */\n  axisFloorLabel?: string | null\n  /**\n   * Snap the gutter's steps to round dB values (1 / 2 / 3 / 6 / 12) so a 3 dB\n   * axis reads -1 / -2 / -3 and a 24 dB axis reads -6 / -12 / -18. Falls back\n   * to the even `axisDb / dbTickCount` division when no round step both lands\n   * on the floor and stays near the requested count. Defaults to true.\n   */\n  niceDbTicks?: boolean\n  /** Hide the dB gutter entirely (and reclaim its width). Defaults to false. */\n  hideDbAxis?: boolean\n\n  /**\n   * The hard reduction budget, in dB. Drawn as a dashed line with a label —\n   * dashed because it is a limit, not a measurement. Omit for no ceiling.\n   */\n  ceilingDb?: number\n  /** Label beside the ceiling line. Defaults to the dB value. */\n  ceilingLabel?: string\n\n  /**\n   * Playhead position, 0..1 across the window. `null` draws no playhead.\n   * Omit entirely to derive one from `transport` / `bpm` via `useBeatClock`.\n   */\n  phase?: number | null\n  /** Host transport for the derived clock. See `lib/beat-clock`. */\n  transport?: BeatClockTransport | null\n  /** Fallback tempo for the derived clock's free-run. */\n  bpm?: number\n  /** Park the derived clock at phase 0. Defaults to true (running). */\n  running?: boolean\n  /**\n   * Reports which clock the drawn playhead came from, so a caption can say\n   * \"host-locked\" or \"free-running\" instead of implying a sync it lacks. Only\n   * fires when the clock is derived — an explicit `phase` is the caller's own.\n   */\n  onClockChange?: (source: BeatClockSource) => void\n\n  /** The event lanes beneath the plot. Omit or empty for a plot-only scope. */\n  lanes?: readonly BeatGridLane[]\n  /** Height of one lane row, in layout units. Defaults to 16. */\n  laneHeight?: number\n\n  /**\n   * The signal layer. A render function receiving the resolved layout, so the\n   * consumer's paths share this plot's exact coordinate space.\n   */\n  children?: (layout: BeatGridLayout) => ReactNode\n  /** A layer drawn UNDER the grid — a waveform the grid should sit on top of. */\n  renderBackground?: (layout: BeatGridLayout) => ReactNode\n\n  /** Caption under the scope. The place for the prediction/measurement note. */\n  caption?: ReactNode\n  /** Accessible name for the plot. */\n  ariaLabel?: string\n\n  /** Layout-box width. Defaults to 320; the SVG scales to its container. */\n  viewWidth?: number\n  /** Plot height in layout units, excluding the lane strip. Defaults to 150. */\n  plotHeight?: number\n  /** Scale axis/lane typography and its gutter together for larger displays. */\n  textScale?: number\n  /** CSS height of the rendered SVG. Defaults to the layout height in px. */\n  height?: number | string\n  /** CSS width. Defaults to \"100%\". */\n  width?: number | string\n\n  /** Override the accent. Otherwise reads --uikit-accent. */\n  color?: string\n  /** Override the secondary accent (ceiling, warn). Otherwise --uikit-accent-2. */\n  accent2?: string\n  className?: string\n  style?: CSSProperties\n}\n\n/** Left gutter width when the dB axis is shown, in layout units. */\nconst DB_GUTTER = 30\n/** Left gutter width for the lane labels — also the plot's left inset. */\nconst LANE_GUTTER = 54\n/** Vertical gap between the plot and the lane strip, in layout units. */\nconst LANE_STRIP_GAP = 6\n\nfunction clamp01(value: number): number {\n  if (!Number.isFinite(value)) return 0\n  return value < 0 ? 0 : value > 1 ? 1 : value\n}\n\n/**\n * Which phase the scope should actually draw, given what the caller passed and\n * what the derived clock says.\n *\n * Pure and exported because this is the decision that can silently lie. Three\n * rules, in order:\n *\n *   1. An explicit `phase` always wins — including `null`, which means \"draw no\n *      playhead\". A caller that owns its clock is never overridden.\n *   2. Otherwise the derived clock is used, except while it is `stopped`, which\n *      also draws nothing. A stopped transport must show an empty plot, not a\n *      cursor pinned at zero, because a cursor at zero looks like a position.\n *   3. Anything non-finite is treated as no playhead rather than drawn at an\n *      undefined x.\n */\nexport function resolveScopePhase(\n  explicit: number | null | undefined,\n  controlled: boolean,\n  clock: { phase: number; source: BeatClockSource }\n): number | null {\n  if (controlled) {\n    if (explicit === null || explicit === undefined) return null\n    if (!Number.isFinite(explicit)) return null\n    return clamp01(explicit)\n  }\n  if (clock.source === \"stopped\") return null\n  return clamp01(clock.phase)\n}\n\n/**\n * The scope's vertical budget: plot, optional gap, lane rows.\n *\n * Exported so the arithmetic that decides the viewBox is asserted rather than\n * eyeballed — a lane strip that is one row too short clips its bottom lane, and\n * that is only visible at a size nobody tests at.\n */\nexport function beatGridScopeHeights(\n  plotHeight: number,\n  laneCount: number,\n  laneHeight: number\n): { stripHeight: number; totalHeight: number } {\n  const rows = Math.max(0, Math.round(laneCount))\n  const rowHeight =\n    Number.isFinite(laneHeight) && laneHeight > 0 ? laneHeight : 16\n  const stripHeight = rows > 0 ? LANE_STRIP_GAP + rows * rowHeight : 0\n  return { stripHeight, totalHeight: plotHeight + stripHeight }\n}\n\n/**\n * Left gutter width: wide enough for lane labels when there are lanes, wide\n * enough for dB labels when the axis is shown, and a bare inset when neither.\n *\n * One function so the plot and the lane strip cannot end up with different left\n * edges — which would put an onset tick at a different x from the curve above\n * it, the exact failure the shared axis exists to prevent.\n */\n/**\n * The dB-tick options the scope hands the geometry.\n *\n * Pure and exported because the distinction it draws is invisible on screen\n * until it is wrong: `axisFloorLabel` UNSET means \"let the geometry print the\n * depth the axis actually reaches\", while `axisFloorLabel={null}` means \"print\n * nothing there\". Collapsing the two — the obvious way to write this, since\n * both are falsy — turns the honest default into a blank gutter, and a blank\n * gutter is exactly as uninformative as the \"-∞\" this replaced.\n */\nexport function beatGridScopeDbTickOptions(\n  dbTickCount: number,\n  niceDbTicks: boolean,\n  axisFloorLabel: string | null | undefined\n): BeatGridDbTickOptions {\n  const base: BeatGridDbTickOptions = {\n    count: dbTickCount,\n    niceSteps: niceDbTicks,\n  }\n  if (axisFloorLabel === undefined) return base\n  return { ...base, floorLabel: axisFloorLabel }\n}\n\nexport function beatGridScopeGutter(\n  hideDbAxis: boolean,\n  laneCount: number\n): number {\n  const hasLanes = laneCount > 0\n  if (hideDbAxis) return hasLanes ? LANE_GUTTER : 8\n  return hasLanes ? Math.max(DB_GUTTER, LANE_GUTTER) : DB_GUTTER\n}\n\nexport function BeatGridScope({\n  bars = 2,\n  beatsPerBar = 4,\n  subdivisions = 4,\n  axisDb = 24,\n  minAxisDb = 6,\n  dbTickCount = 4,\n  axisFloorLabel,\n  niceDbTicks = true,\n  hideDbAxis = false,\n  ceilingDb,\n  ceilingLabel,\n  phase,\n  transport = null,\n  bpm,\n  running = true,\n  onClockChange,\n  lanes,\n  laneHeight = 16,\n  children,\n  renderBackground,\n  caption,\n  ariaLabel,\n  viewWidth = 320,\n  textScale = 1,\n  plotHeight = 150,\n  height,\n  width = \"100%\",\n  color,\n  accent2,\n  className,\n  style,\n}: BeatGridScopeProps) {\n  const accent = color ?? token.accent\n  const secondary = accent2 ?? token.accent2\n  const rawId = useId()\n  const clipId = `uikit-beat-grid-${rawId.replace(/[^a-zA-Z0-9]/g, \"\")}`\n\n  // The clock is always subscribed to (hooks cannot be conditional), but only\n  // consulted when the caller did not supply a phase of its own.\n  const derived = useBeatClock({ transport, bpm, bars, beatsPerBar, running })\n  const controlled = phase !== undefined\n  const clockSource = derived.source\n\n  useEffect(() => {\n    if (controlled) return\n    onClockChange?.(clockSource)\n    // `onClockChange` is deliberately not a dependency: a face that passes an\n    // inline arrow would otherwise re-notify on every render.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [controlled, clockSource])\n\n  const resolvedPhase = resolveScopePhase(phase, controlled, derived)\n\n  const laneRows = lanes ?? []\n  const { stripHeight, totalHeight } = beatGridScopeHeights(\n    plotHeight,\n    laneRows.length,\n    laneHeight\n  )\n  const typeScale = Number.isFinite(textScale)\n    ? Math.min(3, Math.max(0.5, textScale))\n    : 1\n  const gutter = beatGridScopeGutter(hideDbAxis, laneRows.length) * typeScale\n\n  const layout = createBeatGridLayout({\n    bars,\n    beatsPerBar,\n    subdivisions,\n    width: viewWidth,\n    height: plotHeight,\n    left: gutter,\n    right: 8,\n    top: 10,\n    bottom: 12,\n    axisDb,\n    minAxisDb,\n  })\n\n  const dbTicks = hideDbAxis\n    ? []\n    : layout.dbTicks(\n        beatGridScopeDbTickOptions(dbTickCount, niceDbTicks, axisFloorLabel)\n      )\n  const ceilingY =\n    ceilingDb !== undefined && Number.isFinite(ceilingDb)\n      ? layout.ceilingY(ceilingDb)\n      : null\n  const playheadX =\n    resolvedPhase === null\n      ? null\n      : layout.xForBeat(resolvedPhase * layout.totalBeats)\n\n  const plotTop = layout.top\n  const plotBottom = layout.top + layout.plotHeight\n  const plotLeft = layout.left\n  const plotRight = layout.left + layout.plotWidth\n\n  const label =\n    ariaLabel ??\n    `Beat grid scope: ${layout.bars} bar${layout.bars === 1 ? \"\" : \"s\"}, ${layout.axisDb} dB axis${\n      laneRows.length > 0\n        ? `, ${laneRows.length} event lane${laneRows.length === 1 ? \"\" : \"s\"}`\n        : \"\"\n    }`\n\n  const tickWeight = (kind: \"bar\" | \"beat\" | \"sub\") =>\n    kind === \"bar\" ? 1.4 : kind === \"beat\" ? 0.8 : 0.5\n  const tickOpacity = (kind: \"bar\" | \"beat\" | \"sub\") =>\n    kind === \"bar\" ? 0.85 : kind === \"beat\" ? 0.45 : 0.2\n\n  return (\n    <div\n      data-beat-grid-scope=\"\"\n      data-clock={controlled ? undefined : clockSource}\n      className={className}\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 4,\n        width,\n        minWidth: 0,\n        fontFamily: token.font,\n        ...style,\n      }}\n    >\n      <svg\n        role=\"img\"\n        aria-label={label}\n        viewBox={`0 0 ${viewWidth} ${totalHeight}`}\n        preserveAspectRatio=\"none\"\n        style={{\n          display: \"block\",\n          width: \"100%\",\n          height: height ?? totalHeight,\n          background: token.surface,\n          border: `1px solid ${token.border}`,\n          borderRadius: token.radius,\n        }}\n      >\n        <defs>\n          {/* Everything the consumer draws is clipped to the plot, so a curve\n              that runs past the axis cannot spill over the gutter or the\n              lane strip. */}\n          <clipPath id={clipId}>\n            <rect\n              x={plotLeft}\n              y={plotTop}\n              width={layout.plotWidth}\n              height={layout.plotHeight}\n            />\n          </clipPath>\n        </defs>\n\n        {/* Consumer background layer — a waveform the grid sits on top of. */}\n        {renderBackground ? (\n          <g data-beat-grid-background=\"\" clipPath={`url(#${clipId})`}>\n            {renderBackground(layout)}\n          </g>\n        ) : null}\n\n        {/* Vertical grid. Bars heaviest, then beats, then the sixteenths that\n            make an off-grid onset read as off-grid. */}\n        <g data-beat-grid-lines=\"\" aria-hidden>\n          {layout.ticks.map((tick, i) => (\n            <line\n              key={`tick-${i}`}\n              x1={tick.x}\n              x2={tick.x}\n              y1={plotTop}\n              y2={plotBottom + (laneRows.length > 0 ? stripHeight : 0)}\n              stroke={token.border}\n              strokeWidth={tickWeight(tick.kind)}\n              opacity={tickOpacity(tick.kind)}\n            />\n          ))}\n        </g>\n\n        {/* dB gutter: horizontal rules with their labels. 0 dB is the top rule\n            and is drawn heavier — it is the reference everything hangs from. */}\n        {dbTicks.length > 0 ? (\n          <g data-beat-grid-db-axis=\"\" aria-hidden>\n            {dbTicks.map((tick) => (\n              <g key={`db-${tick.db}`}>\n                <line\n                  x1={plotLeft}\n                  x2={plotRight}\n                  y1={tick.y}\n                  y2={tick.y}\n                  stroke={token.border}\n                  strokeWidth={tick.db === 0 ? 1 : 0.5}\n                  opacity={tick.db === 0 ? 0.7 : 0.25}\n                />\n                <text\n                  x={plotLeft - 6}\n                  y={tick.y}\n                  textAnchor=\"end\"\n                  dominantBaseline=\"central\"\n                  fontFamily={token.fontMono}\n                  fontSize={8 * typeScale}\n                  fill={token.textMuted}\n                  style={{ userSelect: \"none\" }}\n                >\n                  {tick.label}\n                </text>\n              </g>\n            ))}\n          </g>\n        ) : null}\n\n        {/* Consumer signal layer, in the plot's own coordinate space. */}\n        {children ? (\n          <g data-beat-grid-layer=\"\" clipPath={`url(#${clipId})`}>\n            {children(layout)}\n          </g>\n        ) : null}\n\n        {/* The budget. Dashed, because a limit is not a measurement. */}\n        {ceilingY !== null ? (\n          <g data-beat-grid-ceiling=\"\" aria-hidden>\n            <line\n              x1={plotLeft}\n              x2={plotRight}\n              y1={ceilingY}\n              y2={ceilingY}\n              stroke={secondary}\n              strokeWidth={1.2}\n              strokeDasharray=\"5 4\"\n              opacity={0.9}\n            />\n            <text\n              x={plotRight - 2}\n              y={ceilingY - 4}\n              textAnchor=\"end\"\n              fontFamily={token.fontMono}\n              fontSize={8 * typeScale}\n              fill={secondary}\n              style={{ userSelect: \"none\" }}\n            >\n              {ceilingLabel ??\n                `-${Number.isInteger(Math.abs(ceilingDb as number)) ? Math.abs(ceilingDb as number) : Math.abs(ceilingDb as number).toFixed(1)} dB`}\n            </text>\n          </g>\n        ) : null}\n\n        {/* The lane strip. Same xForBeat as the plot, so an onset lines up with\n            the curve that answered it. */}\n        {laneRows.length > 0 ? (\n          <g data-beat-grid-lanes=\"\">\n            {laneRows.map((lane, index) => {\n              const top = plotBottom + LANE_STRIP_GAP + index * laneHeight\n              const mid = top + laneHeight / 2\n              const laneColor = lane.color ?? accent\n              return (\n                <g\n                  key={lane.id}\n                  data-beat-grid-lane=\"\"\n                  data-lane-id={lane.id}\n                  data-lane-events={lane.events.length}\n                >\n                  <line\n                    x1={plotLeft}\n                    x2={plotRight}\n                    y1={top}\n                    y2={top}\n                    stroke={token.border}\n                    strokeWidth={0.5}\n                    opacity={0.3}\n                  />\n                  {/* The lane's own pointer, in the gutter beside its name —\n                      the same mark a per-lane readout uses, so the two agree\n                      without the reader having to match them up by position. */}\n                  {lane.marker ? (\n                    <text\n                      data-beat-grid-lane-marker=\"\"\n                      x={2}\n                      y={mid}\n                      dominantBaseline=\"central\"\n                      fontSize={7 * typeScale}\n                      fill={laneColor}\n                      aria-hidden\n                      style={{ userSelect: \"none\" }}\n                    >\n                      {lane.marker}\n                    </text>\n                  ) : null}\n                  <text\n                    x={9 * typeScale}\n                    y={mid}\n                    dominantBaseline=\"central\"\n                    fontFamily={token.font}\n                    fontSize={7 * typeScale}\n                    fontWeight={700}\n                    letterSpacing=\"0.16em\"\n                    fill={token.textMuted}\n                    style={{ userSelect: \"none\" }}\n                  >\n                    {lane.label}\n                  </text>\n                  {lane.events.map((event, eventIndex) => {\n                    const strength = clamp01(event.strength ?? 1)\n                    const x = layout.xForBeat(event.beat)\n                    // Height and opacity both track strength, so a ghost note\n                    // reads as a ghost the way it does on the drum lane.\n                    const tickHeight = Math.max(\n                      3,\n                      (laneHeight - 5) * (0.4 + 0.6 * strength)\n                    )\n                    const opacity = 0.4 + 0.6 * strength\n                    const recovery =\n                      event.recoveryBeat !== undefined &&\n                      Number.isFinite(event.recoveryBeat) &&\n                      event.recoveryBeat > event.beat\n                        ? layout.xForBeat(event.recoveryBeat)\n                        : null\n                    return (\n                      <g\n                        key={`${lane.id}-${eventIndex}`}\n                        data-beat-grid-event=\"\"\n                      >\n                        {recovery !== null && recovery > x ? (\n                          <line\n                            x1={x}\n                            x2={recovery}\n                            y1={mid}\n                            y2={mid}\n                            stroke={laneColor}\n                            strokeWidth={1}\n                            opacity={opacity * 0.4}\n                          />\n                        ) : null}\n                        <rect\n                          x={x - 1}\n                          y={mid - tickHeight / 2}\n                          width={2}\n                          height={tickHeight}\n                          rx={0.6}\n                          fill={event.ghost ? \"none\" : laneColor}\n                          stroke={event.ghost ? laneColor : \"none\"}\n                          strokeWidth={event.ghost ? 0.7 : 0}\n                          opacity={opacity}\n                        />\n                      </g>\n                    )\n                  })}\n                </g>\n              )\n            })}\n          </g>\n        ) : null}\n\n        {/* Gutter divider, drawn last over the grid so it stays crisp. */}\n        <line\n          x1={plotLeft}\n          x2={plotLeft}\n          y1={plotTop}\n          y2={plotBottom + stripHeight}\n          stroke={token.border}\n          strokeWidth={0.8}\n          opacity={0.55}\n        />\n\n        {/* The playhead runs the full height, plot and lanes, because an onset\n            and the curve that answered it must be read against one cursor. */}\n        {playheadX !== null ? (\n          <g\n            data-beat-grid-playhead=\"\"\n            aria-hidden\n            style={{ pointerEvents: \"none\" }}\n          >\n            <line\n              x1={playheadX}\n              x2={playheadX}\n              y1={plotTop}\n              y2={plotBottom + stripHeight}\n              stroke={accent}\n              strokeWidth={1.4}\n              opacity={0.9}\n            />\n            <path\n              d={`M ${playheadX - 3.5} ${plotTop} L ${playheadX + 3.5} ${plotTop} L ${playheadX} ${plotTop + 4.5} Z`}\n              fill={accent}\n            />\n          </g>\n        ) : null}\n      </svg>\n\n      {caption !== undefined && caption !== null ? (\n        <div\n          data-beat-grid-caption=\"\"\n          style={{\n            color: token.textMuted,\n            fontSize: 10,\n            letterSpacing: \"0.04em\",\n            lineHeight: 1.3,\n          }}\n        >\n          {caption}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/beat-grid-scope.tsx"
    }
  ]
}
