{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-xy-pad",
  "type": "registry:ui",
  "title": "XY Pad",
  "description": "A two-dimensional drag surface with a grid, crosshair and a glowing dot that leaves a short trail. Two independent value outputs, each with its own range.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-cc-binding-editor.json",
    "https://pluginui.com/r/core-xy-pad-headless.json"
  ],
  "files": [
    {
      "path": "components/ui/xy-pad.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/xy-pad\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 * XYPad — a two-dimensional drag surface.\n *\n * Grid, crosshair, and a dot that leaves a short trail behind the pointer.\n * Two independent values out, each with its own range and step. The Y axis\n * reads bottom-up, the way a filter/resonance pad is expected to.\n *\n * Keyboard: arrows move the dot on both axes, Shift for a fine pass,\n * Home/End park it at the corners.\n *\n * The gesture — absolute positioning, the drag, the keyboard's coarse arrows\n * and fine Shift pass, and the y-axis inversion that screen coordinates make\n * so easy to get backwards — lives in `XYPadHeadless`, the same split the\n * knob, fader and toggle each have. What is left here is looks, the trail,\n * the readout, double-click-to-default and the two CC drop halves.\n *\n * It wears the kit's five shared skins (see `lib/control-skin`), read here as\n * the dot's face, how loud the crosshair and grid are, and whether the plane\n * carries a wash of accent behind the dot. A `flat` pad and a `flat` knob on\n * one panel are meant to look like the same instrument.\n */\n\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\"\nimport type { CSSProperties } from \"react\"\nimport {\n  normalize,\n  sanitize,\n} from \"maruaudio/ui/shared/math\"\nimport {\n  defaultFormat,\n  type ValueFormatter,\n} from \"maruaudio/ui/shared/format\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  accentFill,\n  CONTROL_SKINS,\n  VINTAGE_FACE,\n  type ControlSkin,\n  type SkinTraits,\n} from \"maruaudio/ui/shared/control-skin\"\nimport { useControlSkin } from \"maruaudio/ui/shared/control-skin-context\"\nimport { XYPadHeadless } from \"@/components/ui/xy-pad-headless\"\nimport {\n  CcControlMenuPanel,\n  ccControlMenuActions,\n} from \"@/components/ui/cc-binding-editor\"\nimport {\n  ContextMenuTarget,\n  type ContextMenuAction,\n} from \"maruaudio/ui/shared/context-menu-target\"\nimport {\n  CcBadge,\n  ccBadgeSlot,\n  useCcActivityPulse,\n  useCcDropTarget,\n  useCcMappingFlash,\n  useControlMidi,\n  type ControlMidiProps,\n} from \"maruaudio/ui/shared/cc-dnd\"\n\nexport interface XYAxisConfig {\n  min?: number\n  max?: number\n  step?: number\n  label?: string\n  unit?: string\n  format?: ValueFormatter\n  /** Stable parameter path used by an ambient ControlMidiProvider. */\n  path?: string\n}\n\nexport interface XYPadMidiProps {\n  /** MIDI mapping for the horizontal axis. */\n  x?: ControlMidiProps\n  /** MIDI mapping for the vertical axis. */\n  y?: ControlMidiProps\n}\n\n/** The XY pad takes the shared skins unchanged — it has no extras of its own. */\nexport type XYPadVariant = ControlSkin\n\n/** What a skin means on a plane rather than on a knob or a fader. */\ninterface XYPadVariantSpec extends SkinTraits {\n  /** Dot diameter in pixels. */\n  dotSize: number\n  /** Crosshair opacity, at rest and while the pad is being dragged. */\n  crosshair: { rest: number; active: number }\n  /** Grid line opacity. 0 hides the grid however many divisions are asked for. */\n  gridOpacity: number\n  /** A wash of accent on the plane behind the dot. */\n  wash: boolean\n}\n\nfunction padSpec(\n  skin: SkinTraits,\n  plane: Omit<XYPadVariantSpec, keyof SkinTraits>\n): XYPadVariantSpec {\n  return { ...skin, ...plane }\n}\n\nconst XY_PAD_VARIANTS: Record<XYPadVariant, XYPadVariantSpec> = {\n  default: padSpec(CONTROL_SKINS.default, {\n    dotSize: 16,\n    crosshair: { rest: 0.28, active: 0.5 },\n    gridOpacity: 0.25,\n    wash: true,\n  }),\n  // Matte: a solid dot, no wash, and quieter furniture behind it.\n  flat: padSpec(CONTROL_SKINS.flat, {\n    dotSize: 14,\n    crosshair: { rest: 0.2, active: 0.42 },\n    gridOpacity: 0.16,\n    wash: false,\n  }),\n  // A cream bead on a dark plane, with the grid drawn as engraving.\n  vintage: padSpec(CONTROL_SKINS.vintage, {\n    dotSize: 15,\n    crosshair: { rest: 0.34, active: 0.55 },\n    gridOpacity: 0.34,\n    wash: false,\n  }),\n  // Nothing but the crosshair and the dot: no grid, no wash.\n  minimal: padSpec(CONTROL_SKINS.minimal, {\n    dotSize: 11,\n    crosshair: { rest: 0.4, active: 0.6 },\n    gridOpacity: 0,\n    wash: false,\n  }),\n  // Lit at rest: the dot keeps its halo whether or not anyone is dragging.\n  neon: padSpec(CONTROL_SKINS.neon, {\n    dotSize: 14,\n    crosshair: { rest: 0.3, active: 0.55 },\n    gridOpacity: 0.18,\n    wash: true,\n  }),\n}\n\nexport interface XYPadProps {\n  /** Optional author-supplied hover text; no default tooltip. */\n  title?: string\n  /** Controlled position. Omit for uncontrolled operation. */\n  value?: { x: number; y: number }\n  /** Starting position when uncontrolled, and the double-click reset target. */\n  defaultValue?: { x: number; y: number }\n  onChange?: (value: { x: number; y: number }) => void\n  onChangeEnd?: (value: { x: number; y: number }) => void\n\n  x?: XYAxisConfig\n  y?: XYAxisConfig\n\n  width?: number | string\n  /**\n   * Upper bound on the rendered width, so a `width: \"100%\"` pad fills its\n   * column only up to a sane size instead of stretching edge to edge. Defaults\n   * to 300 (px). Pass a string for other units, or `null` to remove the cap.\n   */\n  maxWidth?: number | string | null\n  height?: number\n  /**\n   * Visual preset, shared with every other control in the kit. Omitted, the\n   * pad takes whatever a surrounding `ControlSkinProvider` declares.\n   */\n  variant?: XYPadVariant\n  /** Grid divisions per axis. 0 hides the grid. */\n  grid?: number\n  /** Show the formatted x/y readout in the corner. */\n  showReadout?: boolean\n  /** Number of trail samples kept behind the dot. 0 disables the trail. */\n  trailLength?: number\n\n  /**\n   * MIDI mappings for the two axes. While a CC chip is dragged, the plane\n   * reveals an X half and a Y half; drop on the half you want to bind.\n   */\n  midi?: XYPadMidiProps\n\n  disabled?: boolean\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nconst DEFAULT_AXIS: Required<Pick<XYAxisConfig, \"min\" | \"max\" | \"step\">> = {\n  min: 0,\n  max: 1,\n  step: 0,\n}\n\nexport function XYPad({\n  title,\n  value,\n  defaultValue = { x: 0.5, y: 0.5 },\n  onChange,\n  onChangeEnd,\n  x: xConfig,\n  y: yConfig,\n  width = \"100%\",\n  maxWidth = 300,\n  height = 180,\n  variant,\n  grid = 4,\n  showReadout = true,\n  trailLength = 10,\n  midi,\n  disabled = false,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: XYPadProps) {\n  // The pad's variants *are* the shared skins, so one lookup covers both the\n  // explicit prop and whatever the surrounding panel declared.\n  const spec =\n    XY_PAD_VARIANTS[useControlSkin(variant)] ?? XY_PAD_VARIANTS.default\n  // The dot is the pad's face: a cream bead on a vintage panel, a flat accent\n  // where the skin is matte, the two-stop accent gradient otherwise.\n  const dotFace =\n    spec.cap === \"vintage\"\n      ? VINTAGE_FACE\n      : spec.solidFill\n        ? token.accent\n        : accentFill(false, token.accent2, token.accent, \"145deg\")\n  const ax = { ...DEFAULT_AXIS, ...xConfig }\n  const ay = { ...DEFAULT_AXIS, ...yConfig }\n  const labelId = useId()\n\n  const xMidi = useControlMidi({\n    path: xConfig?.path,\n    label: xConfig?.label ?? \"X axis\",\n    range: { min: ax.min, max: ax.max, step: ax.step },\n    midi: midi?.x,\n  })\n  const yMidi = useControlMidi({\n    path: yConfig?.path,\n    label: yConfig?.label ?? \"Y axis\",\n    range: { min: ay.min, max: ay.max, step: ay.step },\n    midi: midi?.y,\n  })\n  const xPulse = useCcActivityPulse(xMidi?.activity)\n  const yPulse = useCcActivityPulse(yMidi?.activity)\n  const xTarget = useCcDropTarget({\n    id: xMidi?.targetId,\n    path: xConfig?.path,\n    label: xMidi?.targetLabel ?? `${xConfig?.label ?? \"X\"} axis`,\n    onCcDrop: xMidi?.onAssign,\n    accepts: xMidi?.accepts,\n    enabled: Boolean(xMidi) && !xMidi?.disabled,\n    controller: xMidi?.controller,\n    onLearnRequest: xMidi?.onLearn,\n    learning: xMidi?.learning ?? false,\n  })\n  const yTarget = useCcDropTarget({\n    id: yMidi?.targetId,\n    path: yConfig?.path,\n    label: yMidi?.targetLabel ?? `${yConfig?.label ?? \"Y\"} axis`,\n    onCcDrop: yMidi?.onAssign,\n    accepts: yMidi?.accepts,\n    enabled: Boolean(yMidi) && !yMidi?.disabled,\n    controller: yMidi?.controller,\n    onLearnRequest: yMidi?.onLearn,\n    learning: yMidi?.learning ?? false,\n  })\n  const xFlash = useCcMappingFlash(\n    xMidi?.assignment,\n    xTarget,\n    xMidi?.controller\n  )\n  const yFlash = useCcMappingFlash(\n    yMidi?.assignment,\n    yTarget,\n    yMidi?.controller\n  )\n  const [axisEditor, setAxisEditor] = useState<{\n    axis: \"X\" | \"Y\"\n    mode: \"assign\" | \"edit\"\n  } | null>(null)\n  const clean = useCallback(\n    (next: { x: number; y: number }) => ({\n      x: sanitize(next.x, ax.min, ax.max, ax.step),\n      y: sanitize(next.y, ay.min, ay.max, ay.step),\n    }),\n    [ax.min, ax.max, ax.step, ay.min, ay.max, ay.step]\n  )\n\n  const isControlled = value !== undefined\n  const [internal, setInternal] = useState(() => clean(defaultValue))\n  const current = isControlled ? clean(value) : internal\n\n  const latest = useRef(current)\n  latest.current = current\n\n  const [trail, setTrail] = useState<ReadonlyArray<{ x: number; y: number }>>(\n    []\n  )\n\n  const commit = useCallback(\n    (next: { x: number; y: number }) => {\n      const value_ = clean(next)\n      if (!isControlled) setInternal(value_)\n      if (value_.x !== latest.current.x || value_.y !== latest.current.y) {\n        onChange?.(value_)\n      }\n      if (trailLength > 0) {\n        const tx = normalize(value_.x, ax.min, ax.max)\n        const ty = normalize(value_.y, ay.min, ay.max)\n        setTrail((prev) => [...prev, { x: tx, y: ty }].slice(-trailLength))\n      }\n    },\n    [clean, isControlled, onChange, trailLength, ax.min, ax.max, ay.min, ay.max]\n  )\n\n  const xFormat = useMemo(\n    () =>\n      xConfig?.format ??\n      defaultFormat(ax.step > 0 ? ax.step : 0.01, xConfig?.unit),\n    [xConfig?.format, xConfig?.unit, ax.step]\n  )\n  const yFormat = useMemo(\n    () =>\n      yConfig?.format ??\n      defaultFormat(ay.step > 0 ? ay.step : 0.01, yConfig?.unit),\n    [yConfig?.format, yConfig?.unit, ay.step]\n  )\n\n  const axisMenuActions: ContextMenuAction[] = []\n  let sourceMenuAction: ContextMenuAction | undefined\n  for (const [axis, control] of [\n    [\"X\", xMidi],\n    [\"Y\", yMidi],\n  ] as const) {\n    if (!control) continue\n    const config = axis === \"X\" ? xConfig : yConfig\n    const axisValue = axis === \"X\" ? current.x : current.y\n    const formatValue = axis === \"X\" ? xFormat : yFormat\n    const midiAction = ccControlMenuActions(\n      control,\n      (mode) => setAxisEditor({ axis, mode }),\n      {\n        label: config?.label ?? `${axis} axis`,\n        value: formatValue(axisValue),\n        ...(config?.path ? { path: config.path } : {}),\n      }\n    ).find((action) => action.id === \"midi-cc\")\n    for (const action of midiAction?.submenu?.actions ?? []) {\n      if (action.id === \"cc-sources\") {\n        sourceMenuAction ??= action\n        continue\n      }\n      axisMenuActions.push({\n        ...action,\n        id: `${axis.toLowerCase()}-${action.id}`,\n        label: (\n          <>\n            {axis}: {action.label}\n          </>\n        ),\n      })\n    }\n  }\n  if (sourceMenuAction) axisMenuActions.push(sourceMenuAction)\n  const menuActions: ContextMenuAction[] =\n    axisMenuActions.length > 0\n      ? [\n          {\n            id: \"midi-cc\",\n            label: \"MIDI CC\",\n            submenu: {\n              ariaLabel: \"XY pad MIDI CC mapping\",\n              actions: axisMenuActions,\n            },\n          },\n        ]\n      : []\n\n  const handleDoubleClick = () => {\n    if (disabled) return\n    const reset = clean(defaultValue)\n    if (!isControlled) setInternal(reset)\n    onChange?.(reset)\n    onChangeEnd?.(reset)\n    setTrail([])\n  }\n\n  const tx = normalize(current.x, ax.min, ax.max)\n  const ty = normalize(current.y, ay.min, ay.max)\n  const left = `${tx * 100}%`\n  const bottom = `${ty * 100}%`\n\n  const gridLines =\n    grid > 0\n      ? Array.from({ length: grid - 1 }, (_, i) => ((i + 1) / grid) * 100)\n      : []\n\n  return (\n    <ContextMenuTarget\n      actions={menuActions}\n      includeAmbientActions\n      style={{\n        display: \"inline-block\",\n        position: \"relative\",\n        width,\n        ...(maxWidth == null ? {} : { maxWidth }),\n      }}\n    >\n      <XYPadHeadless\n        className={`uikit-control${className ? ` ${className}` : \"\"}`}\n        value={current}\n        x={{ min: ax.min, max: ax.max, step: ax.step }}\n        y={{ min: ay.min, max: ay.max, step: ay.step }}\n        onValueChange={commit}\n        // The trail is a record of one gesture, so it starts empty and is let go\n        // of at the end rather than snapping away mid-fade.\n        onGestureStart={() => setTrail([])}\n        onGestureEnd={() => {\n          onChangeEnd?.(latest.current)\n          setTrail([])\n        }}\n        disabled={disabled}\n        aria-label={ariaLabel ?? \"XY pad\"}\n        title={title}\n        aria-describedby={showReadout ? labelId : undefined}\n        onDoubleClick={handleDoubleClick}\n        style={{\n          position: \"relative\",\n          width,\n          ...(maxWidth == null ? {} : { maxWidth }),\n          height,\n          borderRadius: token.radius,\n          background: spec.wash\n            ? `radial-gradient(120% 120% at ${left} calc(100% - ${bottom}), color-mix(in srgb, ${token.accent} 18%, transparent), transparent 70%), ${token.surface}`\n            : token.surface,\n          border: `1px solid ${token.border}`,\n          overflow: \"hidden\",\n          cursor: disabled ? \"not-allowed\" : \"crosshair\",\n          fontFamily: token.font,\n          ...style,\n        }}\n      >\n        {({ dragging }) => (\n          <>\n            {/* Two equal drop zones make the plane itself the target while keeping\n          the assignment unambiguous. They do not intercept pad gestures. */}\n            {xMidi && (\n              <span\n                ref={xTarget.ref}\n                {...xTarget.targetProps}\n                {...(xFlash ? { \"data-uikit-cc-flash\": \"\" } : {})}\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  inset: \"0 50% 0 0\",\n                  zIndex: 5,\n                  pointerEvents: xTarget.mapMode ? \"auto\" : \"none\",\n                  background:\n                    xTarget.isOver || xTarget.mapMode\n                      ? `color-mix(in srgb, ${token.accent} 18%, transparent)`\n                      : \"transparent\",\n                  boxShadow:\n                    xPulse || xFlash\n                      ? `inset 0 0 18px ${token.accent}`\n                      : undefined,\n                  transition:\n                    \"background 120ms ease-out, box-shadow 160ms ease-out\",\n                }}\n              >\n                {xTarget.armed && (\n                  <AxisDropHint axis=\"X\" active={xTarget.isOver} />\n                )}\n              </span>\n            )}\n            {yMidi && (\n              <span\n                ref={yTarget.ref}\n                {...yTarget.targetProps}\n                {...(yFlash ? { \"data-uikit-cc-flash\": \"\" } : {})}\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  inset: \"0 0 0 50%\",\n                  zIndex: 5,\n                  pointerEvents: yTarget.mapMode ? \"auto\" : \"none\",\n                  background:\n                    yTarget.isOver || yTarget.mapMode\n                      ? `color-mix(in srgb, ${token.accent} 18%, transparent)`\n                      : \"transparent\",\n                  boxShadow:\n                    yPulse || yFlash\n                      ? `inset 0 0 18px ${token.accent}`\n                      : undefined,\n                  transition:\n                    \"background 120ms ease-out, box-shadow 160ms ease-out\",\n                }}\n              >\n                {yTarget.armed && (\n                  <AxisDropHint axis=\"Y\" active={yTarget.isOver} />\n                )}\n              </span>\n            )}\n            {/* Grid */}\n            {spec.gridOpacity > 0 &&\n              gridLines.map((pct) => (\n                <span\n                  key={`v${pct}`}\n                  aria-hidden\n                  style={gridLineStyle(\"vertical\", pct, spec.gridOpacity)}\n                />\n              ))}\n            {spec.gridOpacity > 0 &&\n              gridLines.map((pct) => (\n                <span\n                  key={`h${pct}`}\n                  aria-hidden\n                  style={gridLineStyle(\"horizontal\", pct, spec.gridOpacity)}\n                />\n              ))}\n\n            {/* Crosshair */}\n            <span\n              aria-hidden\n              style={{\n                position: \"absolute\",\n                left,\n                top: 0,\n                bottom: 0,\n                width: 1,\n                background: token.accent,\n                opacity: dragging ? spec.crosshair.active : spec.crosshair.rest,\n              }}\n            />\n            <span\n              aria-hidden\n              style={{\n                position: \"absolute\",\n                bottom,\n                left: 0,\n                right: 0,\n                height: 1,\n                background: token.accent,\n                opacity: dragging ? spec.crosshair.active : spec.crosshair.rest,\n              }}\n            />\n\n            {/* Trail — oldest sample is faintest */}\n            {trail.map((point, i) => {\n              const age = (i + 1) / trail.length\n              return (\n                <span\n                  key={i}\n                  aria-hidden\n                  style={{\n                    position: \"absolute\",\n                    left: `${point.x * 100}%`,\n                    bottom: `${point.y * 100}%`,\n                    width: 8 * age,\n                    height: 8 * age,\n                    marginLeft: (-8 * age) / 2,\n                    marginBottom: (-8 * age) / 2,\n                    borderRadius: 999,\n                    background: token.accent2,\n                    opacity: age * 0.35,\n                    pointerEvents: \"none\",\n                  }}\n                />\n              )\n            })}\n\n            {/* Dot */}\n            <span\n              aria-hidden\n              className={dragging ? undefined : \"uikit-animated\"}\n              style={{\n                position: \"absolute\",\n                left,\n                bottom,\n                width: spec.dotSize,\n                height: spec.dotSize,\n                marginLeft: -spec.dotSize / 2,\n                marginBottom: -spec.dotSize / 2,\n                borderRadius: 999,\n                background: dotFace,\n                // `ink` belongs on the face: it rims a cream bead legibly, while the\n                // crosshair stays on the accent because it is drawn on the plane,\n                // where a dark ink would simply disappear.\n                border: `1px solid ${spec.ink ?? \"rgba(255,255,255,0.55)\"}`,\n                // The dot's halo is the pad's lit part, so it follows the shared\n                // rule: brighter while live, and a skin that glows at rest keeps a\n                // softer version of it when nothing is happening.\n                boxShadow: dragging\n                  ? `0 0 20px 4px color-mix(in srgb, ${token.accent} 70%, transparent)`\n                  : spec.glowAtRest\n                    ? `0 0 16px 3px color-mix(in srgb, ${token.accent} 60%, transparent)`\n                    : `0 0 12px 1px color-mix(in srgb, ${token.accent} 45%, transparent)`,\n                pointerEvents: \"none\",\n              }}\n            />\n\n            {/* Axis captions */}\n            {(xConfig?.label || yConfig?.label) && (\n              <>\n                {xConfig?.label && (\n                  <span\n                    aria-hidden\n                    style={{ ...axisCaption, right: 8, bottom: 6 }}\n                  >\n                    {xConfig.label} →\n                  </span>\n                )}\n                {yConfig?.label && (\n                  <span\n                    aria-hidden\n                    style={{\n                      ...axisCaption,\n                      left: 6,\n                      top: 8,\n                      writingMode: \"vertical-rl\",\n                      transform: \"rotate(180deg)\",\n                    }}\n                  >\n                    {yConfig.label} →\n                  </span>\n                )}\n              </>\n            )}\n\n            {/* Readout */}\n            {showReadout && (\n              <span\n                id={labelId}\n                style={{\n                  position: \"absolute\",\n                  top: 8,\n                  right: 8,\n                  fontFamily: token.fontMono,\n                  fontSize: 10,\n                  fontVariantNumeric: \"tabular-nums\",\n                  color: token.textMuted,\n                  background: \"rgba(0,0,0,0.25)\",\n                  borderRadius: 4,\n                  padding: \"2px 6px\",\n                  pointerEvents: \"none\",\n                }}\n              >\n                {xFormat(current.x)} · {yFormat(current.y)}\n              </span>\n            )}\n\n            {(xMidi?.assignment || yMidi?.assignment) && (\n              <span\n                style={{\n                  position: \"absolute\",\n                  left: 8,\n                  top: 8,\n                  zIndex: 6,\n                  display: \"flex\",\n                  gap: 5,\n                }}\n              >\n                {xMidi?.assignment && (\n                  <AxisBadge\n                    axis=\"X\"\n                    midi={xMidi}\n                    assignment={xMidi.assignment}\n                    active={xPulse}\n                  />\n                )}\n                {yMidi?.assignment && (\n                  <AxisBadge\n                    axis=\"Y\"\n                    midi={yMidi}\n                    assignment={yMidi.assignment}\n                    active={yPulse}\n                  />\n                )}\n              </span>\n            )}\n          </>\n        )}\n      </XYPadHeadless>\n      {axisEditor && (axisEditor.axis === \"X\" ? xMidi : yMidi) && (\n        <CcControlMenuPanel\n          key={axisEditor.axis}\n          midi={(axisEditor.axis === \"X\" ? xMidi : yMidi)!}\n          mode={axisEditor.mode}\n          onClose={() => setAxisEditor(null)}\n        />\n      )}\n    </ContextMenuTarget>\n  )\n}\n\nfunction AxisDropHint({ axis, active }: { axis: \"X\" | \"Y\"; active: boolean }) {\n  return (\n    <span\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"grid\",\n        placeItems: \"center\",\n        color: active ? token.accent : token.textMuted,\n        fontFamily: token.fontMono,\n        fontSize: 11,\n        fontWeight: 700,\n        letterSpacing: \"0.08em\",\n      }}\n    >\n      DROP · {axis}\n    </span>\n  )\n}\n\nfunction AxisBadge({\n  axis,\n  midi,\n  assignment,\n  active,\n}: {\n  axis: \"X\" | \"Y\"\n  midi: ControlMidiProps\n  assignment: NonNullable<ControlMidiProps[\"assignment\"]>\n  active: boolean\n}) {\n  const slot = ccBadgeSlot(midi, assignment, active)\n  return (\n    <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 3 }}>\n      <span\n        style={{\n          color: token.textMuted,\n          fontFamily: token.fontMono,\n          fontSize: 8,\n        }}\n      >\n        {axis}\n      </span>\n      {midi.renderBadge?.(slot) ?? (\n        <CcBadge\n          assignment={assignment}\n          onClear={midi.onClear}\n          active={active}\n          learning={midi.learning ?? false}\n          controller={midi.controller}\n          sourceTargetId={midi.targetId}\n          onMove={midi.onClear}\n          movesAtomically={midi.movesAtomically}\n          {...(midi.binding ? { binding: midi.binding } : {})}\n          {...(midi.range ? { range: midi.range } : {})}\n        />\n      )}\n    </span>\n  )\n}\n\nconst axisCaption: CSSProperties = {\n  position: \"absolute\",\n  fontSize: 9,\n  fontWeight: 600,\n  letterSpacing: \"0.08em\",\n  textTransform: \"uppercase\",\n  color: token.textMuted,\n  pointerEvents: \"none\",\n}\n\nfunction gridLineStyle(\n  direction: \"vertical\" | \"horizontal\",\n  pct: number,\n  opacity = 0.25\n): CSSProperties {\n  return {\n    position: \"absolute\",\n    background: token.border,\n    opacity,\n    ...(direction === \"vertical\"\n      ? { left: `${pct}%`, top: 0, bottom: 0, width: 1 }\n      : { top: `${pct}%`, left: 0, right: 0, height: 1 }),\n  }\n}\n",
      "type": "registry:component",
      "target": "@ui/xy-pad.tsx"
    }
  ]
}
