{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-drum-lane",
  "type": "registry:ui",
  "title": "Drum Lane",
  "description": "The rhythm sibling of the piano roll: named lanes (Kick, Snare, Hat…) down the left, a beat grid across, and every hit drawn at its exact onset so timing and swing are legible — a round marker that visibly sits off the gridline when a hit is pushed or swung, over a sixteenth grid that stays countable while it plays. Velocity is the marker's size and opacity, accents wear a ring, and sustained hits grow a tail. It shares the piano roll's whole beat axis, host-driven playhead, active-hit highlight and --uikit-* theming through the same geometry core (createLaneLayout), changing only what a drum view needs: taller rows, text labels instead of a keyboard, and hits placed by lane instead of pitch. The playhead is a prop the host drives; null draws no cursor and no readout, so a stopped transport shows nothing pinned at zero.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/drum-lane.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/drum-lane\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 * DrumLane — the rhythm sibling of PianoRoll: named lanes across a beat grid,\n * with hits drawn so their TIMING is legible.\n *\n * ## Why this exists next to PianoRoll rather than inside it\n *\n * A drum pattern is a piano roll with two things changed: its rows are named\n * lanes (Kick, Snare, Hat…) instead of pitches, and what you need to read off\n * it is not melody but *time* — exactly when each hit lands, and whether the\n * groove is pushed or swung. So it shares the half that is the same — the beat\n * axis, the two-tone bar/beat grid, the host-driven playhead, the active-hit\n * highlight, the whole `--uikit-*` theming — by building on the SAME geometry\n * core (`lib/piano-roll-geometry`'s `createLaneLayout`, which is `xForBeat` and\n * friends unchanged, only the rows re-addressed by lane index). It changes only\n * the half that differs: taller rows with text labels, and a hit that carries a\n * precise ONSET MARKER.\n *\n * ## Reading timing and swing\n *\n * The old rhythm preview drew each hit as a small dot centred in its step cell,\n * which quantised the picture: a straight sixteenth and a swung one landed in\n * the same place, so you could not see the swing at all. Here a hit is drawn at\n * its real beat position — a bright round marker at the exact onset, with an\n * optional tail for its length — over a grid whose sixteenth lines are actually\n * visible. A hit that sits a little to the right of its gridline IS a\n * pushed/swung hit, and now it looks like one.\n *\n * ## The gutter\n *\n * Lane names sit in a fixed-width gutter, set in the kit's regular (sans)\n * font. A name that is too long for the gutter is shortened with an ellipsis —\n * measured against the real rendered text, not guessed from character counts —\n * and the whole gutter is clipped, so a label can never spill over the first\n * beats of the grid. The full name stays available as a hover title.\n *\n * ## Who owns what\n *\n * The HOST owns the clock. `playhead` is a beat position, or null — the same\n * contract PianoRoll keeps: null draws no cursor and no readout, so a stopped\n * transport shows nothing pinned at zero. The host owns the hits too; the\n * component keeps only view state (which hit is selected). SSR-safe: geometry is\n * pure maths and all DOM access lives in handlers and effects.\n */\n\nimport {\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport type { CSSProperties, PointerEvent as ReactPointerEvent } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport { svgMarkerPath } from \"maruaudio/ui/shared/svg-markers\"\nimport {\n  createLaneLayout,\n  formatBarsBeatsFromBeats,\n} from \"maruaudio/ui/shared/piano-roll-geometry\"\n\n/** One drum lane: an id the hits reference, and a label for the gutter. */\nexport interface DrumLaneRow {\n  /** Stable id a hit's `lane` field points at. */\n  readonly id: string\n  /** Gutter caption — the drum's name. */\n  readonly label: string\n  /** An override accent for this whole lane's hits. */\n  readonly color?: string\n}\n\n/** One hit. Placed by lane and beat — never by pitch. */\nexport interface DrumHit {\n  /** Stable identity across renders — selection keys on it. */\n  readonly id: string\n  /** Which lane it sits on — a `DrumLaneRow.id`. */\n  readonly lane: string\n  /** Onset in beats from the start. Its exact value is what shows swing. */\n  readonly startBeats: number\n  /** Length in beats. 0 (or omitted) draws a pure onset marker with no tail. */\n  readonly durationBeats?: number\n  /** 0..127. Marker size and opacity, so a ghost note reads as a ghost note. */\n  readonly velocity?: number\n  /** Accent hit — drawn with an outline ring, the hardware convention. */\n  readonly accent?: boolean\n}\n\nexport type DrumLaneScheme = \"dark\" | \"light\" | \"inherit\"\n\nconst LIGHT_SCHEME_VARS: CSSProperties = {\n  \"--uikit-surface\": \"#f7f7f4\",\n  \"--uikit-text\": \"#1b1e27\",\n  \"--uikit-text-muted\": \"rgba(27, 30, 39, 0.55)\",\n  \"--uikit-border\": \"rgba(27, 30, 39, 0.28)\",\n} as CSSProperties\n\nconst DARK_SCHEME_VARS: CSSProperties = {\n  \"--uikit-surface\": \"#14161d\",\n  \"--uikit-text\": \"#e8eaf0\",\n  \"--uikit-text-muted\": \"rgba(232, 234, 240, 0.55)\",\n  \"--uikit-border\": \"rgba(148, 163, 184, 0.35)\",\n} as CSSProperties\n\nexport interface DrumLaneProps {\n  /** The lanes, top to bottom. */\n  lanes: readonly DrumLaneRow[]\n  /** The hits. Any hit whose `lane` matches no row is dropped. */\n  hits: readonly DrumHit[]\n  /** Bars in the pattern. */\n  bars: number\n  /** Quarter-note beats per bar. Defaults to 4. */\n  beatsPerBar?: number\n\n  /** Absolute beat the host's clock is on, or null for no cursor. */\n  playhead?: number | null\n  /**\n   * Light up a hit while the playhead is on it, for live feedback on what is\n   * sounding. A hit is active while the playhead is within a small window of its\n   * onset (or inside its tail, if it has one). Defaults to true; no effect while\n   * the playhead is null.\n   */\n  highlightActiveHits?: boolean\n\n  /**\n   * Grid lines per beat: 4 draws the sixteenth grid drummers program on, which\n   * is what makes an off-grid (swung, pushed) hit visible. Bar and beat lines\n   * are always drawn and heaviest. Defaults to 4.\n   */\n  subdivisions?: number\n  /** Show the bar.beat.sixteenth transport readout while playing. Default true. */\n  showPosition?: boolean\n\n  /** Selection reporting. Present = hits are selectable. Absent = read-only. */\n  onSelectionChange?: (selectedIds: string[]) => void\n  /** Controlled selection. Omit to let the lane keep its own. */\n  selectedIds?: readonly string[]\n  /** Fired on a plain click / Enter / Space on a hit — the hook to audition it. */\n  onHitActivate?: (hit: DrumHit) => void\n\n  scheme?: DrumLaneScheme\n  /** Row height in pixels. Drum rows are tall on purpose. Defaults to 30. */\n  rowHeight?: number\n  /**\n   * Gutter width for the lane labels, in layout px. Defaults to 60. Labels\n   * that do not fit are ellipsized (and the gutter is clipped), never allowed\n   * to overflow onto the grid.\n   */\n  gutterWidth?: number\n  width?: number | string\n  /** Override the accent for this instance. */\n  color?: string\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\n/** Faintest a hit is drawn, however low its velocity. */\nconst MIN_HIT_OPACITY = 0.45\n/** How near the onset (in beats) the playhead counts as \"on\" a tail-less hit. */\nconst ACTIVE_WINDOW_BEATS = 0.12\n/** Inset of a lane label from the gutter's left edge, in layout px. */\nconst LABEL_PAD_LEFT = 8\n/** Breathing room a label keeps from the gutter divider, in layout px. */\nconst LABEL_PAD_RIGHT = 6\nconst ELLIPSIS = \"\\u2026\"\n\n/** Layout effect in the browser, plain effect (a no-op at render) on the server. */\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect\n\n/**\n * Shorten `label` with an ellipsis until `measure` says it fits in `available`.\n * `measure` reports the rendered width of a candidate string in the same units\n * as `available` (SVG user units here). Binary-searches the cut so a long GM\n * name costs a handful of measurements, not one per character.\n */\nexport function fitLabel(\n  label: string,\n  available: number,\n  measure: (text: string) => number\n): string {\n  if (label.length === 0 || measure(label) <= available) return label\n  if (available <= 0) return \"\"\n  const candidate = (keep: number) => label.slice(0, keep).trimEnd() + ELLIPSIS\n  // Largest `keep` whose ellipsized prefix still fits; 0 means only \"…\" fits.\n  let lo = 0\n  let hi = label.length - 1\n  while (lo < hi) {\n    const mid = Math.ceil((lo + hi) / 2)\n    if (measure(candidate(mid)) <= available) lo = mid\n    else hi = mid - 1\n  }\n  const fitted = candidate(lo)\n  return measure(fitted) <= available ? fitted : \"\"\n}\n\nexport function DrumLane({\n  lanes,\n  hits,\n  bars,\n  beatsPerBar = 4,\n  playhead = null,\n  highlightActiveHits = true,\n  subdivisions = 4,\n  showPosition = true,\n  onSelectionChange,\n  selectedIds,\n  onHitActivate,\n  scheme = \"inherit\",\n  rowHeight = 30,\n  gutterWidth = 60,\n  width = \"100%\",\n  color,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: DrumLaneProps) {\n  const accent = color ?? token.accent\n  const selectable = typeof onSelectionChange === \"function\"\n  const rawId = useId()\n  const gutterClipId = `uikit-drum-lane-gutter-${rawId.replace(/[^a-zA-Z0-9]/g, \"\")}`\n\n  const laneIndex = useMemo(() => {\n    const map = new Map<string, number>()\n    lanes.forEach((lane, index) => map.set(lane.id, index))\n    return map\n  }, [lanes])\n\n  const [measuredWidth, setMeasuredWidth] = useState<number | null>(null)\n  const layout = useMemo(\n    () =>\n      createLaneLayout({\n        lanes: lanes.length,\n        bars,\n        beatsPerBar,\n        rowHeight,\n        gutter: gutterWidth,\n        ...(measuredWidth !== null ? { width: measuredWidth } : {}),\n      }),\n    [lanes.length, bars, beatsPerBar, rowHeight, gutterWidth, measuredWidth]\n  )\n\n  const [internalSelection, setInternalSelection] = useState<readonly string[]>(\n    []\n  )\n  const selection = selectedIds ?? internalSelection\n  const selectionSet = useMemo(() => new Set(selection), [selection])\n  const setSelection = (next: string[]) => {\n    setInternalSelection(next)\n    onSelectionChange?.(next)\n  }\n\n  const svgRef = useRef<SVGSVGElement | null>(null)\n  useIsomorphicLayoutEffect(() => {\n    const el = svgRef.current\n    if (!el || typeof ResizeObserver === \"undefined\") return\n    const measure = () => {\n      const next = Math.round(el.getBoundingClientRect().width)\n      if (next > 0) setMeasuredWidth((prev) => (prev === next ? prev : next))\n    }\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(el)\n    return () => observer.disconnect()\n  }, [])\n\n  /* ---------------------------------------------------------- lane labels */\n\n  // Regular (sans) type, sized to the row: 11px on a comfortable row, never\n  // below 8px on the tightest compact rows.\n  const labelFontSize = Math.max(8, Math.min(11, layout.rowHeight - 4))\n  const labelAvailable = Math.max(\n    0,\n    layout.gutter - LABEL_PAD_LEFT - LABEL_PAD_RIGHT\n  )\n\n  // Ellipsize labels that overflow the gutter. Measured against a hidden\n  // <text> that shares the labels' font, so the cut reflects the real rendered\n  // width (the kit's font is a CSS variable — nothing to measure with a canvas).\n  // Runs in a layout effect so the shortened label paints in the same frame.\n  // `fittedLabels` holds only the labels that changed; anything else renders\n  // as given. On the server, and anywhere text cannot be measured, the labels\n  // render whole and the gutter clip still keeps them off the grid.\n  const measureRef = useRef<SVGTextElement | null>(null)\n  // The host often hands over a fresh `lanes` array every render (and renders\n  // at playhead rate), so the fit is keyed on the label CONTENT, not the array:\n  // a stable signature, memoized back into the (id, label) pairs the effect\n  // reads. Re-measuring only when a label actually changes keeps the forced\n  // text layouts off the animation frame.\n  const labelsKey = JSON.stringify(lanes.map((lane) => [lane.id, lane.label]))\n  const laneLabels = useMemo(\n    () => JSON.parse(labelsKey) as readonly (readonly [string, string])[],\n    [labelsKey]\n  )\n  const [fittedLabels, setFittedLabels] = useState<ReadonlyMap<string, string>>(\n    () => new Map()\n  )\n  useIsomorphicLayoutEffect(() => {\n    const node = measureRef.current\n    if (!node || typeof node.getComputedTextLength !== \"function\") return\n    const measure = (text: string) => {\n      node.textContent = text\n      return node.getComputedTextLength()\n    }\n    const next = new Map<string, string>()\n    for (const [id, label] of laneLabels) {\n      const fitted = fitLabel(label, labelAvailable, measure)\n      if (fitted !== label) next.set(id, fitted)\n    }\n    node.textContent = \"\"\n    setFittedLabels((prev) => {\n      if (prev.size !== next.size) return next\n      for (const [id, text] of next) if (prev.get(id) !== text) return next\n      return prev\n    })\n    // `measuredWidth` re-measures once the element is actually laid out, e.g.\n    // after mounting inside a hidden tab where text lengths read as zero.\n  }, [laneLabels, labelAvailable, labelFontSize, measuredWidth])\n\n  /* ------------------------------------------------------------- geometry */\n\n  const beatLines = useMemo(() => {\n    const perBeatDivisions = Math.max(1, Math.round(subdivisions))\n    const lines: { beat: number; kind: \"bar\" | \"beat\" | \"sub\" }[] = []\n    const total = layout.totalBeats * perBeatDivisions\n    for (let i = 0; i <= total; i += 1) {\n      const beat = i / perBeatDivisions\n      const kind =\n        beat % layout.beatsPerBar === 0\n          ? \"bar\"\n          : Number.isInteger(beat)\n            ? \"beat\"\n            : \"sub\"\n      lines.push({ beat, kind })\n    }\n    return lines\n  }, [subdivisions, layout])\n\n  /* -------------------------------------------------------------- drawing */\n\n  const svgHeight = layout.height\n  const positionText =\n    playhead !== null && Number.isFinite(playhead)\n      ? formatBarsBeatsFromBeats(playhead, layout.beatsPerBar)\n      : null\n  const playheadX =\n    playhead !== null &&\n    Number.isFinite(playhead) &&\n    playhead >= 0 &&\n    playhead <= layout.totalBeats\n      ? layout.xForBeat(playhead)\n      : null\n\n  const label =\n    ariaLabel ??\n    `Drum lanes: ${lanes.length} lane${lanes.length === 1 ? \"\" : \"s\"}, ${hits.length} hit${\n      hits.length === 1 ? \"\" : \"s\"\n    } across ${layout.bars} bar${layout.bars === 1 ? \"\" : \"s\"}${selectable ? \"\" : \" (read-only)\"}`\n\n  const schemeVars =\n    scheme === \"light\"\n      ? LIGHT_SCHEME_VARS\n      : scheme === \"dark\"\n        ? DARK_SCHEME_VARS\n        : undefined\n\n  const lineOpacity = (kind: \"bar\" | \"beat\" | \"sub\") =>\n    kind === \"bar\" ? 0.9 : kind === \"beat\" ? 0.5 : 0.22\n  const lineWidth = (kind: \"bar\" | \"beat\" | \"sub\") =>\n    kind === \"bar\" ? 1.4 : kind === \"beat\" ? 0.8 : 0.5\n\n  const selectHit = (hit: DrumHit, additive: boolean) => {\n    if (!selectable) return\n    if (additive) {\n      const next = selectionSet.has(hit.id)\n        ? selection.filter((id) => id !== hit.id)\n        : [...selection, hit.id]\n      setSelection(next)\n    } else {\n      setSelection([hit.id])\n    }\n  }\n\n  const onBackgroundPointerDown = (\n    event: ReactPointerEvent<SVGRectElement>\n  ) => {\n    if (!selectable) return\n    if (!event.shiftKey && !event.metaKey) setSelection([])\n  }\n\n  return (\n    <div\n      className={className}\n      style={{\n        ...schemeVars,\n        width,\n        fontFamily: token.font,\n        position: \"relative\",\n        ...style,\n      }}\n      role={selectable ? \"group\" : \"img\"}\n      aria-label={label}\n    >\n      {showPosition && positionText ? (\n        <div\n          aria-hidden\n          style={{\n            position: \"absolute\",\n            top: 4,\n            right: 6,\n            zIndex: 2,\n            pointerEvents: \"none\",\n          }}\n        >\n          <span\n            style={{\n              fontFamily: token.fontMono,\n              fontSize: 11,\n              fontVariantNumeric: \"tabular-nums\",\n              color: token.text,\n              background: token.surface,\n              border: `1px solid ${token.border}`,\n              borderRadius: 4,\n              padding: \"1px 5px\",\n            }}\n          >\n            {positionText}\n          </span>\n        </div>\n      ) : null}\n\n      <svg\n        ref={svgRef}\n        role=\"img\"\n        aria-label={label}\n        viewBox={`0 0 ${layout.width} ${svgHeight}`}\n        preserveAspectRatio=\"none\"\n        style={{\n          display: \"block\",\n          width: \"100%\",\n          height: svgHeight,\n          background: token.surface,\n          border: `1px solid ${token.border}`,\n          borderRadius: token.radius,\n        }}\n      >\n        {/* Background click target — clears selection. */}\n        <rect\n          x={0}\n          y={0}\n          width={layout.width}\n          height={svgHeight}\n          fill=\"transparent\"\n          onPointerDown={onBackgroundPointerDown}\n        />\n\n        {/* Lane row bands — alternate a hair of tint so the lanes stay distinct\n            even where a lane is empty for a while. */}\n        {lanes.map((lane, index) => {\n          const rect = layout.laneRect(index)\n          return (\n            <rect\n              key={`lane-band-${lane.id}`}\n              x={layout.gutter}\n              y={rect.y}\n              width={rect.width}\n              height={rect.height}\n              fill={token.text}\n              opacity={index % 2 === 1 ? 0.04 : 0}\n            />\n          )\n        })}\n\n        {/* Vertical grid: bars heavy, beats medium, sixteenths faint — the\n            sixteenth lines are what make an off-grid hit read as off-grid. */}\n        {beatLines.map((line, i) => (\n          <line\n            key={`grid-${i}`}\n            x1={layout.xForBeat(line.beat)}\n            x2={layout.xForBeat(line.beat)}\n            y1={0}\n            y2={svgHeight}\n            stroke={token.border}\n            strokeWidth={lineWidth(line.kind)}\n            opacity={lineOpacity(line.kind)}\n          />\n        ))}\n\n        {/* Lane separators. */}\n        {Array.from({ length: lanes.length + 1 }, (_, row) => (\n          <line\n            key={`lane-sep-${row}`}\n            x1={layout.gutter}\n            x2={layout.width}\n            y1={row * layout.rowHeight}\n            y2={row * layout.rowHeight}\n            stroke={token.border}\n            strokeWidth={0.5}\n            opacity={0.3}\n          />\n        ))}\n\n        {/* Lane labels down the gutter — regular type, ellipsized to fit, and\n            clipped to the gutter as a hard guarantee against overflow. */}\n        <defs>\n          <clipPath id={gutterClipId}>\n            <rect\n              x={0}\n              y={0}\n              width={Math.max(0, layout.gutter - 1)}\n              height={svgHeight}\n            />\n          </clipPath>\n        </defs>\n        <g clipPath={`url(#${gutterClipId})`}>\n          {lanes.map((lane, index) => {\n            const rect = layout.laneRect(index)\n            const text = fittedLabels.get(lane.id) ?? lane.label\n            const shortened = text !== lane.label\n            return (\n              <g key={`lane-label-${lane.id}`}>\n                {shortened ? <desc>{lane.label}</desc> : null}\n                <text\n                  x={LABEL_PAD_LEFT}\n                  y={rect.y + rect.height / 2}\n                  dominantBaseline=\"central\"\n                  fontFamily={token.font}\n                  fontSize={labelFontSize}\n                  fontWeight={400}\n                  fill={token.text}\n                  opacity={0.85}\n                  style={{ userSelect: \"none\" }}\n                >\n                  {text}\n                </text>\n              </g>\n            )\n          })}\n        </g>\n        {/* Hidden measuring text for the ellipsis fit — same font as the\n            labels, never painted, emptied after each pass. */}\n        <text\n          ref={measureRef}\n          aria-hidden\n          x={-1000}\n          y={-1000}\n          visibility=\"hidden\"\n          fontFamily={token.font}\n          fontSize={labelFontSize}\n          fontWeight={400}\n          style={{ pointerEvents: \"none\" }}\n        />\n\n        {/* Gutter divider. */}\n        <line\n          x1={layout.gutter}\n          x2={layout.gutter}\n          y1={0}\n          y2={svgHeight}\n          stroke={token.border}\n          strokeWidth={1}\n          opacity={0.6}\n        />\n\n        {/* The hits. A hit is a round onset marker at its exact beat, plus a\n            tail if it has a duration. Velocity sets size and opacity; accent\n            adds a ring; the active hit lights to full and grows. */}\n        {hits.map((hit) => {\n          const index = laneIndex.get(hit.lane)\n          if (index === undefined) return null\n          const rect = layout.laneRect(index)\n          const cx = layout.xForBeat(hit.startBeats)\n          const cy = rect.y + rect.height / 2\n          const marker = svgMarkerPath(cx, cy)\n          const laneColor = lanes[index]?.color ?? accent\n          const velocity = Math.min(1, Math.max(0, (hit.velocity ?? 100) / 127))\n          const selected = selectionSet.has(hit.id)\n\n          const active =\n            highlightActiveHits &&\n            playhead !== null &&\n            Number.isFinite(playhead) &&\n            (hit.durationBeats && hit.durationBeats > 0\n              ? (playhead as number) >= hit.startBeats &&\n                (playhead as number) < hit.startBeats + hit.durationBeats\n              : Math.abs((playhead as number) - hit.startBeats) <=\n                ACTIVE_WINDOW_BEATS)\n\n          // Marker radius: velocity-scaled, capped to the row, bumped when\n          // active so the sounding hit is the biggest one on screen.\n          const base = Math.min(rect.height * 0.3, 8)\n          const r = base * (0.55 + 0.45 * velocity) * (active ? 1.18 : 1)\n          const opacity = active\n            ? 1\n            : MIN_HIT_OPACITY + (1 - MIN_HIT_OPACITY) * velocity\n\n          const tailWidth =\n            hit.durationBeats && hit.durationBeats > 0\n              ? Math.max(\n                  0,\n                  layout.xForBeat(hit.startBeats + hit.durationBeats) - cx\n                )\n              : 0\n\n          const activate = () => onHitActivate?.(hit)\n\n          return (\n            <g\n              key={hit.id}\n              onClick={(event) => {\n                if (event.shiftKey || event.metaKey) {\n                  selectHit(hit, true)\n                } else {\n                  selectHit(hit, false)\n                  activate()\n                }\n              }}\n              onKeyDown={(event) => {\n                if (event.key === \"Enter\" || event.key === \" \") {\n                  event.preventDefault()\n                  activate()\n                }\n              }}\n              tabIndex={selectable ? 0 : -1}\n              role={selectable ? \"button\" : undefined}\n              aria-label={`${lanes[index]?.label ?? hit.lane} hit at beat ${hit.startBeats.toFixed(2)}${active ? \", sounding\" : \"\"}${selected ? \", selected\" : \"\"}`}\n              aria-pressed={selectable ? selected : undefined}\n              style={{\n                cursor: selectable ? \"pointer\" : \"default\",\n                outline: \"none\",\n              }}\n            >\n              {/* Active halo behind the marker. */}\n              {active ? (\n                <>\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={accent}\n                    strokeWidth={2 * (r + 4)}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                    opacity={0.9}\n                  />\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={token.surface}\n                    strokeWidth={2 * (r + 2)}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                  />\n                </>\n              ) : null}\n              {/* Tail for a sustained hit (an open hat, a ride). */}\n              {tailWidth > 1 ? (\n                <rect\n                  x={cx}\n                  y={cy - r * 0.5}\n                  width={tailWidth}\n                  height={r}\n                  rx={r * 0.5}\n                  fill={laneColor}\n                  opacity={opacity * 0.5}\n                />\n              ) : null}\n              {/* Onset marker — a dot centred on the exact hit time, so a\n                  swung hit visibly sits off its gridline. */}\n              {/* Accent ring — the louder, marked hit. */}\n              {hit.accent ? (\n                <>\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={laneColor}\n                    strokeWidth={2 * (r + 3)}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                    opacity={opacity}\n                  />\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={token.surface}\n                    strokeWidth={2 * (r + 2)}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                  />\n                </>\n              ) : null}\n              {selected ? (\n                <path\n                  d={marker}\n                  fill=\"none\"\n                  stroke={accent}\n                  strokeWidth={2 * r + 1.5}\n                  strokeLinecap=\"round\"\n                  vectorEffect=\"non-scaling-stroke\"\n                  opacity={opacity}\n                />\n              ) : null}\n              <path\n                d={marker}\n                fill=\"none\"\n                stroke={laneColor}\n                strokeWidth={2 * r}\n                strokeLinecap=\"round\"\n                vectorEffect=\"non-scaling-stroke\"\n                opacity={opacity}\n              />\n            </g>\n          )\n        })}\n\n        {/* The playhead — same contract as PianoRoll: nothing when stopped. */}\n        {playheadX !== null ? (\n          <g aria-hidden style={{ pointerEvents: \"none\" }}>\n            <line\n              x1={playheadX}\n              x2={playheadX}\n              y1={0}\n              y2={svgHeight}\n              stroke={accent}\n              strokeWidth={1.75}\n            />\n            <path\n              d={`M ${playheadX - 4} 0 L ${playheadX + 4} 0 L ${playheadX} 5 Z`}\n              fill={accent}\n            />\n          </g>\n        ) : null}\n      </svg>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/drum-lane.tsx"
    }
  ]
}
