{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-eq-editor",
  "type": "registry:ui",
  "title": "EQ Editor",
  "description": "Hands on the EQ curve: N draggable band handles over the eq-display response, wheel for Q, full keyboard access per band, and two readings of the same band model — parametric (curve plus handles) or bars (columns standing at their own frequencies with the curve behind them). Controlled, with the band model, hit testing and step maths in a pure core beside it.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-eq-display.json"
  ],
  "files": [
    {
      "path": "components/ui/eq-editor.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/eq-editor\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 * EQEditor — the interactive layer over `EQDisplay`.\n *\n * `EQDisplay` draws a band stack and stays a pure renderer; this puts hands on\n * it. Drag a handle across the plot to move a band in frequency and gain, wheel\n * over it to change Q, tab to it and use the arrows for all three. N bands, not\n * three: what a band will let you change is a property of the band, so an\n * editor over four peaking bands and an editor over a fixed low/mid/high stack\n * are the same component with different inputs.\n *\n * ## Two modes, one band model\n *\n *   - **`parametric`** — the curve with a handle per band, the shape a\n *     parametric EQ has had since it was hardware.\n *   - **`bars`** — the same bands as columns standing on the 0 dB line at their\n *     own frequencies, with the curve still drawn behind them. The graphic-EQ\n *     reading of the identical data: it makes the *set* of gains legible in one\n *     glance where the curve makes the *result* legible.\n *\n * They are modes rather than two components because the model, the gestures and\n * the keyboard map are identical — only the drawing and one axis restriction\n * differ (a column does not move sideways). Switching modes changes nothing\n * about the bands.\n *\n * ## Controlled, with no copies\n *\n * `bands` in, changes out. Temporary pending writes protect gesture intent\n * while an iframe/native bridge echoes older values; acknowledged host values\n * and rejected-write timeout restore the controlled contract. Selection, hover\n * and drag state never become another persistent parameter store. A consumer wiring handles to individual\n * parameters takes `onBandChange` (index + what changed); one holding an array\n * takes `onBandsChange` (the whole next array). Both fire, so neither consumer\n * has to reconstruct what the other wanted.\n *\n * All the arithmetic — limits, hit testing, drag mapping, step sizes — is in\n * `./eq-band-model`, which is pure and pinned by tests in Node. This file is\n * the drawing and the event plumbing.\n *\n * SSR-safe: the plot is SVG computed from props, and every DOM read happens\n * inside a handler.\n *\n * Styling comes from CSS custom properties — see `lib/theme.ts` — with a\n * `color` prop to override the accent for one instance.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport type {\n  CSSProperties,\n  ReactNode,\n  KeyboardEvent,\n  PointerEvent as ReactPointerEvent,\n} from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  SVG_MARKER_DIAMETER_PX,\n  SVG_MARKER_OUTLINE_DIAMETER_PX,\n  svgMarkerPath,\n} from \"maruaudio/ui/shared/svg-markers\"\nimport { positionToValue01 } from \"maruaudio/ui/shared/knob-core\"\nimport { useSurfacePointer } from \"maruaudio/ui/shared/use-surface-pointer\"\nimport { hasControlledValueAcknowledgement } from \"maruaudio/ui/shared/use-control\"\nimport { useWheel } from \"maruaudio/ui/shared/use-wheel\"\nimport {\n  EQDisplay,\n  EQ_DISPLAY_VIEWBOX,\n  EQ_AXIS_GUTTERS,\n  eqDisplayScale,\n  type EQCursor,\n} from \"@/components/ui/eq-display\"\nimport {\n  applyBandPatch,\n  adaptiveDecibelRange,\n  limitBandFrequency,\n  bandDragPatch,\n  bandKeyPatch,\n  bandHandlePoint,\n  bandWheelPatch,\n  barLayout,\n  describeBand,\n  hitTestBand,\n  hitTestColumn,\n  resolveBandLimits,\n  type BandDefaults,\n  type BandPatch,\n  type BandPoint,\n  type EQEditorBand,\n} from \"maruaudio/ui/shared/eq-band-model\"\n\nexport type {\n  BandDefaults,\n  BandPatch,\n  BandRange,\n  EQEditorBand,\n} from \"maruaudio/ui/shared/eq-band-model\"\n\n/** Which drawing the same bands get. */\nexport type EQEditorMode = \"parametric\" | \"bars\"\n\nexport interface EQEditorProps {\n  /** The bands, and what each will let the editor change. Controlled. */\n  bands: ReadonlyArray<EQEditorBand>\n  /** The whole next array, for a consumer that holds one. */\n  onBandsChange?: (bands: EQEditorBand[]) => void\n  /**\n   * One band's change, for a consumer whose bands are separate parameters.\n   * Fires alongside `onBandsChange`, with the patch and the band it applies to.\n   */\n  onBandChange?: (index: number, patch: BandPatch, band: EQEditorBand) => void\n  /** Fires once when pointer, wheel or keyboard editing ends. */\n  onChangeEnd?: () => void\n\n  /** Controlled selected band; selection is separate from enabled state. */\n  selectedBand?: number | null\n  onBandSelect?: (index: number) => void\n  onBandEditStart?: (index: number) => void\n  onBandEditEnd?: (index: number, band: EQEditorBand) => void\n  onInteraction?: () => void\n  showAxisLabels?: boolean\n  adaptiveGainRange?: boolean\n  showBypassedHandles?: boolean\n  handleLabels?: \"name\" | \"number\" | \"none\"\n  frequencyLimit?: number\n  /** SVG content rendered beneath response curves in the exact plot viewport. */\n  plotBackground?: ReactNode\n\n  mode?: EQEditorMode\n  /** Ranges for any axis a band does not pin itself. */\n  defaults?: BandDefaults\n\n  /** Needed for the bilinear transform behind the curve. */\n  sampleRate?: number\n  width?: number | string\n  height?: number | string\n  minFrequency?: number\n  maxFrequency?: number\n  /** Vertical range, in dB either side of flat. */\n  decibelRange?: number\n\n  /** Draw each band's own response behind the summed curve. */\n  showBands?: boolean\n  showGrid?: boolean\n  /** Fill the area between the curve and the 0 dB line. */\n  fill?: boolean\n  /** Draw each band's label under its handle or column. */\n  showLabels?: boolean\n  /** Draw the vertical guide through each band's frequency. */\n  showGuides?: boolean\n  /** A modulation cursor riding the curve — forwarded to the display. */\n  cursor?: EQCursor | null\n\n  disabled?: boolean\n  /** Override the accent colour for this instance, curve included. */\n  color?: string\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nconst { width: VB_W, height: VB_H, padding: PAD } = EQ_DISPLAY_VIEWBOX\n\n/** Handle hit radius, in viewBox units — generous, per `envelope-display`. */\nconst HIT_RADIUS = 10\n\nexport function EQEditor({\n  bands: authoritativeBands,\n  onBandsChange,\n  onBandChange,\n  onChangeEnd,\n  selectedBand,\n  onBandSelect,\n  onBandEditStart,\n  onBandEditEnd,\n  onInteraction,\n  showAxisLabels = false,\n  adaptiveGainRange = false,\n  showBypassedHandles = false,\n  handleLabels = \"name\",\n  frequencyLimit,\n  plotBackground,\n  mode = \"parametric\",\n  defaults,\n  sampleRate = 48000,\n  width = \"100%\",\n  height = 200,\n  minFrequency = 20,\n  maxFrequency = 20000,\n  decibelRange: requestedDecibelRange = 18,\n  showBands = mode === \"parametric\",\n  showGrid = true,\n  fill = true,\n  showLabels = true,\n  showGuides = mode === \"parametric\",\n  cursor,\n  disabled = false,\n  color,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: EQEditorProps) {\n  // Pending values are gesture intent only; host echoes remain authoritative.\n  const [pending, setPending] = useState<Record<number, BandPatch>>({})\n  const pendingRef = useRef(pending)\n  pendingRef.current = pending\n  const editingRef = useRef<number | null>(null)\n  const wheelTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const callbacks = useRef({\n    onBandEditStart,\n    onBandEditEnd,\n    onChangeEnd,\n    onInteraction,\n  })\n  callbacks.current = {\n    onBandEditStart,\n    onBandEditEnd,\n    onChangeEnd,\n    onInteraction,\n  }\n  const authoritativeSignature = JSON.stringify(authoritativeBands)\n  const projected = authoritativeBands.map((band, index) => ({\n    ...band,\n    ...pending[index],\n  }))\n  const bands =\n    frequencyLimit === undefined\n      ? projected\n      : projected.map((band) => limitBandFrequency(band, frequencyLimit))\n  const decibelRange = adaptiveGainRange\n    ? adaptiveDecibelRange(\n        showBypassedHandles\n          ? bands.map((band) => ({ ...band, bypassed: false }))\n          : bands,\n        requestedDecibelRange\n      )\n    : requestedDecibelRange\n  const scale = useMemo(\n    () => eqDisplayScale({ minFrequency, maxFrequency, decibelRange }),\n    [minFrequency, maxFrequency, decibelRange]\n  )\n\n  const columns = useMemo(\n    () => (mode === \"bars\" ? barLayout(bands, scale) : []),\n    [mode, bands, scale]\n  )\n\n  const overlayRef = useRef<SVGSVGElement | null>(null)\n  const containerRef = useRef<HTMLDivElement | null>(null)\n\n  // Selection and hover are local UI state; the host owns persistent values.\n  const [hover, setHover] = useState<number | null>(null)\n  const [focused, setFocused] = useState<number | null>(null)\n  const [dragIndex, setDragIndex] = useState<number | null>(null)\n\n  // Read inside handlers so a gesture bound for the life of a drag never sees a\n  // stale array, and the anchor a fine pass measures from.\n  const bandsRef = useRef(bands)\n  bandsRef.current = bands\n\n  const endEdit = useCallback(() => {\n    const index = editingRef.current\n    if (index === null) return\n    editingRef.current = null\n    const band = bandsRef.current[index]\n    if (band) callbacks.current.onBandEditEnd?.(index, band)\n    callbacks.current.onChangeEnd?.()\n  }, [])\n  const beginEdit = useCallback(\n    (index: number) => {\n      if (editingRef.current === index) return\n      endEdit()\n      editingRef.current = index\n      callbacks.current.onInteraction?.()\n      callbacks.current.onBandEditStart?.(index)\n    },\n    [endEdit]\n  )\n  useEffect(() => {\n    // Exact echoes acknowledge writes after release. An old echo must not\n    // move a dragged handle; rejected writes recover after the shared 1s grace.\n    if (editingRef.current !== null) return\n    const acknowledged: Record<number, BandPatch> = {}\n    for (const [key, patch] of Object.entries(pending)) {\n      const index = Number(key)\n      const host = authoritativeBands[index]\n      const remaining: BandPatch = {}\n      for (const axis of [\"freq\", \"gain\", \"q\"] as const) {\n        if (\n          patch[axis] !== undefined &&\n          host &&\n          !hasControlledValueAcknowledgement(\n            host[axis] ?? 0,\n            patch[axis]!,\n            false\n          )\n        )\n          remaining[axis] = patch[axis]\n      }\n      if (Object.keys(remaining).length) acknowledged[index] = remaining\n    }\n    if (JSON.stringify(acknowledged) !== JSON.stringify(pending)) {\n      setPending(acknowledged)\n      return\n    }\n    if (!Object.keys(pending).length) return\n    const timer = setTimeout(() => setPending({}), 1000)\n    return () => clearTimeout(timer)\n  }, [authoritativeSignature, pending, dragIndex])\n  useEffect(\n    () => () => {\n      if (wheelTimer.current) clearTimeout(wheelTimer.current)\n      endEdit()\n    },\n    [endEdit]\n  )\n  const dragRef = useRef<number | null>(null)\n  const anchorRef = useRef<BandPoint | null>(null)\n  useEffect(() => {\n    if (!disabled) return\n    if (wheelTimer.current) clearTimeout(wheelTimer.current)\n    dragRef.current = null\n    anchorRef.current = null\n    setDragIndex(null)\n    endEdit()\n  }, [disabled, endEdit])\n\n  /** Pointer position in viewBox units, from the overlay's own rect. */\n  const toViewBox = useCallback(\n    (clientX: number, clientY: number): BandPoint | null => {\n      const svg = overlayRef.current\n      if (!svg) return null\n      const rect = svg.getBoundingClientRect()\n      if (rect.width === 0 || rect.height === 0) return null\n      return {\n        x: positionToValue01(clientX, rect.left, rect.width) * VB_W,\n        y: positionToValue01(clientY, rect.top, rect.height) * VB_H,\n      }\n    },\n    []\n  )\n\n  const commit = useCallback(\n    (index: number, patch: BandPatch | null) => {\n      if (!patch || Object.keys(patch).length === 0) return\n      const current = bandsRef.current\n      const band = current[index]\n      if (!band) return\n      const next = applyBandPatch(current, index, patch)\n      const intent = {\n        ...pendingRef.current,\n        [index]: { ...pendingRef.current[index], ...patch },\n      }\n      pendingRef.current = intent\n      bandsRef.current = next\n      setPending(intent)\n      onBandChange?.(index, patch, next[index])\n      onBandsChange?.(next)\n    },\n    [onBandChange, onBandsChange]\n  )\n\n  /* ------------------------------------------------------------- pointer -- */\n\n  const drag = useSurfacePointer(\n    (state) => {\n      const index = dragRef.current\n      if (index === null) return\n      const band = bandsRef.current[index]\n      if (!band) return\n      const point = toViewBox(state.clientX, state.clientY)\n      if (!point) return\n      if (state.first) anchorRef.current = bandHandlePoint(band, scale)\n      commit(\n        index,\n        bandDragPatch(band, point, scale, {\n          defaults,\n          // A column keeps its place on the frequency axis: the whole point of\n          // the bars reading is that the x positions stay put.\n          axes: mode === \"bars\" ? { freq: false } : undefined,\n          fine: state.fine,\n          anchor: anchorRef.current ?? undefined,\n        })\n      )\n    },\n    {\n      disabled,\n      onEnd: () => {\n        dragRef.current = null\n        anchorRef.current = null\n        setDragIndex(null)\n        endEdit()\n      },\n    }\n  )\n\n  const startDrag = useCallback(\n    (index: number) => (event: ReactPointerEvent<Element>) => {\n      if (disabled || event.button !== 0 || dragRef.current !== null) return\n      event.stopPropagation()\n      onBandSelect?.(index)\n      beginEdit(index)\n      dragRef.current = index\n      setDragIndex(index)\n      setFocused(index)\n      drag.onPointerDown(event)\n    },\n    [disabled, drag, onBandSelect, beginEdit]\n  )\n\n  /**\n   * Which band a bare press on the plot grabs.\n   *\n   * Pressing the background rather than a handle still starts a gesture on the\n   * nearest band — the click-to-jump `use-surface-pointer` exists for. In bars\n   * mode the lane decides, so the empty space above a short column belongs to\n   * that column; in parametric mode it is the nearest handle within reach, and\n   * a press in genuinely open space does nothing rather than teleporting a band\n   * the user never aimed at.\n   */\n  const bandAt = useCallback(\n    (point: BandPoint): number =>\n      mode === \"bars\"\n        ? hitTestColumn(columns, point.x)\n        : hitTestBand(\n            showBypassedHandles\n              ? bandsRef.current.map((band) => ({ ...band, bypassed: false }))\n              : bandsRef.current,\n            point,\n            scale,\n            HIT_RADIUS\n          ),\n    [mode, columns, scale, showBypassedHandles]\n  )\n\n  const onSurfacePointerDown = useCallback(\n    (event: ReactPointerEvent<SVGSVGElement>) => {\n      if (disabled) return\n      const point = toViewBox(event.clientX, event.clientY)\n      if (!point) return\n      const index = bandAt(point)\n      if (index < 0) return\n      startDrag(index)(event)\n    },\n    [disabled, toViewBox, bandAt, startDrag]\n  )\n\n  const onSurfacePointerMove = useCallback(\n    (event: ReactPointerEvent<SVGSVGElement>) => {\n      if (dragRef.current !== null) return\n      const point = toViewBox(event.clientX, event.clientY)\n      const index = point ? bandAt(point) : -1\n      setHover(index < 0 ? null : index)\n    },\n    [toViewBox, bandAt]\n  )\n\n  /* --------------------------------------------------------------- wheel -- */\n\n  // The wheel is bound only while a band with an editable Q is under the\n  // pointer. `useWheel` calls preventDefault on every notch it sees, so binding\n  // it any wider would trap the page's scroll over a plot that has nothing to\n  // do with it — which is exactly what a face whose bands all lock Q (a fixed\n  // three-band shelving EQ, say) would otherwise suffer.\n  const wheelTarget = dragIndex ?? hover\n  const wheelBand = wheelTarget === null ? undefined : bands[wheelTarget]\n  const wheelEditable = wheelBand\n    ? resolveBandLimits(wheelBand, defaults).q !== null\n    : false\n  useWheel(\n    containerRef,\n    (state) => {\n      if (wheelTarget === null) return\n      const band = bandsRef.current[wheelTarget]\n      if (!band) return\n      beginEdit(wheelTarget)\n      onBandSelect?.(wheelTarget)\n      if (wheelTimer.current) clearTimeout(wheelTimer.current)\n      wheelTimer.current = setTimeout(() => {\n        endEdit()\n        setPending((current) => ({ ...current }))\n      }, 150)\n      // Scroll up (negative deltaY) narrows the band: away from you tightens.\n      commit(\n        wheelTarget,\n        bandWheelPatch(band, -state.deltaY, {\n          defaults,\n          fine: state.event.shiftKey,\n        })\n      )\n    },\n    { disabled: disabled || !wheelEditable }\n  )\n\n  /* ------------------------------------------------------------ keyboard -- */\n\n  const onHandleKeyDown = useCallback(\n    (index: number) => (event: KeyboardEvent<SVGGElement>) => {\n      if (disabled) return\n      const band = bandsRef.current[index]\n      if (!band) return\n      const patch = bandKeyPatch(band, event.key, {\n        defaults,\n        fine: event.shiftKey,\n        altKey: event.altKey,\n      })\n      // Unhandled keys and locked axes fall through untouched, so Tab still\n      // moves focus and the arrows still scroll a page this handle cannot use.\n      if (!patch) return\n      event.preventDefault()\n      onBandSelect?.(index)\n      beginEdit(index)\n      commit(index, patch)\n      endEdit()\n    },\n    [disabled, defaults, commit, onBandSelect, beginEdit, endEdit]\n  )\n\n  /* ------------------------------------------------------------- drawing -- */\n\n  const accent = color ?? token.accent\n  const rootStyle: CSSProperties = {\n    width,\n    position: \"relative\",\n    fontFamily: token.font,\n    // Setting the variable rather than passing a colour down means the curve,\n    // the fill and the grid inside EQDisplay all follow one override.\n    ...(color\n      ? ({ [\"--uikit-accent\" as string]: color } as CSSProperties)\n      : null),\n    ...style,\n  }\n\n  const handles = bands.map((band, index) => {\n    const limits = resolveBandLimits(band, defaults)\n    const point = bandHandlePoint(band, scale)\n    const bandColor = band.color ?? accent\n    const active =\n      dragIndex === index ||\n      hover === index ||\n      focused === index ||\n      selectedBand === index\n    // aria-valuenow describes the axis the vertical arrows move, because that\n    // is the one axis every band has; the sentence in valuetext carries the\n    // rest. A band with no gain reports its frequency instead of nothing.\n    const primary = limits.gain\n      ? { now: band.gain ?? 0, min: limits.gain.min, max: limits.gain.max }\n      : {\n          now: band.freq,\n          min: limits.freq?.min ?? band.freq,\n          max: limits.freq?.max ?? band.freq,\n        }\n    return { band, index, limits, point, bandColor, active, primary }\n  })\n\n  return (\n    <div ref={containerRef} className={className} style={rootStyle}>\n      <EQDisplay\n        bands={bands}\n        sampleRate={sampleRate}\n        width=\"100%\"\n        height={height}\n        minFrequency={minFrequency}\n        maxFrequency={maxFrequency}\n        decibelRange={decibelRange}\n        showBands={showBands}\n        // The editor draws its own handles — they have to carry hit areas,\n        // focus rings and colours the display knows nothing about.\n        showHandles={false}\n        showGrid={showGrid}\n        showAxisLabels={showAxisLabels}\n        plotBackground={plotBackground}\n        fill={fill}\n        cursor={cursor}\n        aria-label={ariaLabel ?? `EQ editor, ${bands.length} bands`}\n      />\n\n      <svg\n        ref={overlayRef}\n        viewBox={`0 0 ${VB_W} ${VB_H}`}\n        preserveAspectRatio=\"none\"\n        role=\"group\"\n        aria-label={ariaLabel ?? `EQ editor, ${bands.length} bands`}\n        style={{\n          position: \"absolute\",\n          top: showAxisLabels ? EQ_AXIS_GUTTERS.top : 0,\n          left: showAxisLabels ? EQ_AXIS_GUTTERS.left : 0,\n          width: showAxisLabels\n            ? `calc(100% - ${EQ_AXIS_GUTTERS.left + EQ_AXIS_GUTTERS.right}px)`\n            : \"100%\",\n          height,\n          touchAction: \"none\",\n          cursor: disabled\n            ? \"not-allowed\"\n            : dragIndex !== null\n              ? \"grabbing\"\n              : \"default\",\n        }}\n        onPointerDown={onSurfacePointerDown}\n        onPointerMove={onSurfacePointerMove}\n        onPointerLeave={() => setHover(null)}\n      >\n        {mode === \"bars\" &&\n          columns.map((column) => {\n            const entry = handles[column.index]\n            if (!entry || entry.band.bypassed) return null\n            return (\n              <g key={`col-${column.index}`} pointerEvents=\"none\">\n                {/* The lane: the column's whole hit area, drawn faintly so the\n                    target is visible rather than merely present. */}\n                <rect\n                  x={column.x}\n                  y={PAD}\n                  width={column.width}\n                  height={scale.innerHeight}\n                  fill={entry.bandColor}\n                  opacity={entry.active ? 0.1 : 0.04}\n                />\n                <rect\n                  x={column.x}\n                  y={column.y}\n                  width={column.width}\n                  height={Math.max(column.height, 0.6)}\n                  fill={entry.bandColor}\n                  opacity={entry.active ? 0.85 : 0.6}\n                />\n                {/* The cap, so a band sitting at 0 dB is still a column rather\n                    than a missing one. */}\n                <line\n                  x1={column.x}\n                  y1={column.gainY}\n                  x2={column.x + column.width}\n                  y2={column.gainY}\n                  stroke={entry.bandColor}\n                  strokeWidth={2}\n                  vectorEffect=\"non-scaling-stroke\"\n                />\n              </g>\n            )\n          })}\n\n        {handles.map(({ band, index, point, bandColor, active, primary }) => {\n          if (band.bypassed && !showBypassedHandles) return null\n          const column = mode === \"bars\" ? columns[index] : null\n          const markerX = column ? column.centerX : point.x\n          const markerY = column ? column.gainY : point.y\n          const marker = svgMarkerPath(markerX, markerY)\n          return (\n            <g\n              key={index}\n              role=\"slider\"\n              tabIndex={disabled ? -1 : 0}\n              aria-label={band.label ?? `Band ${index + 1}`}\n              aria-valuemin={primary.min}\n              aria-valuemax={primary.max}\n              aria-valuenow={primary.now}\n              aria-valuetext={describeBand(band, defaults)}\n              aria-disabled={disabled || undefined}\n              onPointerDown={startDrag(index)}\n              onKeyDown={onHandleKeyDown(index)}\n              onFocus={() => {\n                setFocused(index)\n                onBandSelect?.(index)\n              }}\n              onBlur={() =>\n                setFocused((current) => (current === index ? null : current))\n              }\n              style={{\n                cursor: disabled ? \"not-allowed\" : \"grab\",\n                outline: \"none\",\n                opacity: band.bypassed ? 0.45 : 1,\n              }}\n            >\n              {showGuides && (\n                <line\n                  x1={point.x}\n                  y1={PAD}\n                  x2={point.x}\n                  y2={PAD + scale.innerHeight}\n                  stroke={bandColor}\n                  strokeWidth={0.5}\n                  opacity={active ? 0.5 : 0.18}\n                  vectorEffect=\"non-scaling-stroke\"\n                />\n              )}\n\n              {/* Generous invisible target. In bars mode it is the whole lane,\n                  so a short column is as easy to grab as a tall one. */}\n              {column ? (\n                <rect\n                  x={column.x}\n                  y={PAD}\n                  width={column.width}\n                  height={scale.innerHeight}\n                  fill=\"transparent\"\n                />\n              ) : (\n                <circle\n                  cx={point.x}\n                  cy={point.y}\n                  r={HIT_RADIUS}\n                  fill=\"transparent\"\n                />\n              )}\n\n              {/* Focus ring, drawn rather than left to the UA outline: an\n                  outline on an SVG <g> is unreliable across browsers, and this\n                  one follows the handle. */}\n              {focused === index && !disabled ? (\n                <>\n                  {/* The previous focus circle stretched with this plot.\n                      These layered round caps keep its ring circular. */}\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={token.focus}\n                    strokeWidth={18}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                    pointerEvents=\"none\"\n                  />\n                  <path\n                    d={marker}\n                    fill=\"none\"\n                    stroke={token.surface}\n                    strokeWidth={14}\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                    pointerEvents=\"none\"\n                  />\n                </>\n              ) : null}\n              {/* A layered round-cap marker keeps its outline and fill\n                  circular at every aspect ratio. */}\n              <path\n                d={marker}\n                fill=\"none\"\n                data-eq-marker=\"outline\"\n                stroke={token.text}\n                strokeWidth={SVG_MARKER_OUTLINE_DIAMETER_PX}\n                strokeLinecap=\"round\"\n                vectorEffect=\"non-scaling-stroke\"\n                pointerEvents=\"none\"\n              />\n              <path\n                d={marker}\n                fill=\"none\"\n                data-eq-marker=\"fill\"\n                stroke={bandColor}\n                strokeOpacity={0.9}\n                strokeWidth={active ? 10 : SVG_MARKER_DIAMETER_PX}\n                strokeLinecap=\"round\"\n                vectorEffect=\"non-scaling-stroke\"\n                pointerEvents=\"none\"\n              />\n            </g>\n          )\n        })}\n      </svg>\n\n      {((showLabels && handleLabels === \"name\") ||\n        handleLabels === \"number\") && (\n        <div\n          aria-hidden\n          style={{\n            position: \"absolute\",\n            top: showAxisLabels ? EQ_AXIS_GUTTERS.top : 0,\n            left: showAxisLabels ? EQ_AXIS_GUTTERS.left : 0,\n            width: showAxisLabels\n              ? `calc(100% - ${EQ_AXIS_GUTTERS.left + EQ_AXIS_GUTTERS.right}px)`\n              : \"100%\",\n            height,\n            pointerEvents: \"none\",\n          }}\n        >\n          {handles.map(({ band, index, point, bandColor }) =>\n            (band.bypassed && !showBypassedHandles) ||\n            (handleLabels === \"name\" && !band.label) ? null : (\n              <span\n                key={index}\n                data-eq-band-label={handleLabels}\n                data-bypassed={band.bypassed || undefined}\n                data-selected={selectedBand === index || undefined}\n                style={{\n                  \"--uikit-eq-band-color\": bandColor,\n                  position: \"absolute\",\n                  // Percentage of the box, so the label tracks the same\n                  // stretched axis the curve does with no second geometry.\n                  left: `${(point.x / VB_W) * 100}%`,\n                  ...(handleLabels === \"number\"\n                    ? {\n                        top: `${(point.y / VB_H) * 100}%`,\n                        transform: \"translate(-50%, -50%)\",\n                        fontWeight: 700,\n                      }\n                    : { bottom: 4, transform: \"translateX(-50%)\" }),\n                  fontFamily: token.fontMono,\n                  fontSize: 9,\n                  letterSpacing: \"0.04em\",\n                  color: handleLabels === \"number\" ? token.surface : bandColor,\n                  whiteSpace: \"nowrap\",\n                } as CSSProperties & { \"--uikit-eq-band-color\": string }}\n              >\n                {handleLabels === \"number\" ? index + 1 : band.label}\n              </span>\n            )\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/eq-editor.tsx"
    }
  ]
}
