{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-xy-pad-headless",
  "type": "registry:ui",
  "title": "XY Pad Headless",
  "description": "The unstyled plane primitive an XY pad is built on: absolute pointer positioning on both axes, the y-axis inversion screen coordinates make easy to get backwards, and keyboard stepping where the plain arrow is coarse and Shift is fine. Renders no appearance — a render prop hands your face the drag state.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/xy-pad-headless.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/xy-pad-headless\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 * XYPadHeadless — the unstyled plane primitive an XY pad is built on.\n *\n * The fourth headless layer, and the one with the least code and the most\n * conventions. Two of them are easy to get backwards:\n *\n * - Screen Y grows downward and a pad's Y grows upward, so the vertical axis\n *   is inverted on the way in. A pad that reads the raw coordinate feels\n *   upside down and every face built on it inherits the mistake.\n * - The keyboard's *plain* arrow is the coarse step (five) and Shift is the\n *   fine one (a single step) — the opposite of the drag, where Shift slows\n *   things down. It is deliberate: a plane needs to be crossable by keyboard\n *   in a sane number of presses.\n *\n * Positioning is absolute, like a fader's: pressing anywhere puts the dot\n * there and the gesture carries on into a drag.\n *\n * Like the other primitives it owns no value state — it reports where the\n * pointer or the keyboard put the two values and the host decides. Trails,\n * formatters, double-click-to-default and MIDI CC live above it.\n *\n * The pointer mapping and the keyboard convention themselves are pure\n * functions in `lib/plane-geometry`, shared with the multi-point `PlanPad`, so\n * a one-point pad and an N-point plan cannot drift apart on either.\n */\n\nimport { forwardRef, useCallback, useRef } from \"react\"\nimport type { ComponentProps, KeyboardEvent, ReactNode } from \"react\"\nimport { normalize } from \"maruaudio/ui/shared/math\"\nimport { useDrag } from \"maruaudio/ui/shared/use-control\"\nimport {\n  planeAxisValue,\n  planeFractionAtPointer,\n  planeKeyStep,\n  type PlaneAxisBounds,\n  type PlanePosition,\n} from \"maruaudio/ui/shared/plane-geometry\"\n\n/** One axis' bounds. `step` of 0 or undefined means continuous. */\nexport type XYAxisBounds = PlaneAxisBounds\n\nexport type XYPadPoint = PlanePosition\n\n/** What a face is handed on every render. */\nexport interface XYPadRenderState {\n  /** True while the pointer is down on the plane. */\n  dragging: boolean\n  /** The position normalised to [0..1], y already the right way up. */\n  x01: number\n  y01: number\n}\n\ntype NativeDivProps = Omit<\n  ComponentProps<\"div\">,\n  \"role\" | \"tabIndex\" | \"children\" | \"onChange\"\n>\n\nexport interface XYPadHeadlessProps extends NativeDivProps {\n  value: XYPadPoint\n  x: XYAxisBounds\n  y: XYAxisBounds\n  onValueChange: (next: XYPadPoint) => void\n  /** Fires when a pointer gesture begins — a trail wants to reset here. */\n  onGestureStart?: () => void\n  /** Fires when one ends — good for undo coalescing. */\n  onGestureEnd?: () => void\n  disabled?: boolean\n  children: (state: XYPadRenderState) => ReactNode\n}\n\nexport const XYPadHeadless = forwardRef<HTMLDivElement, XYPadHeadlessProps>(\n  function XYPadHeadless(\n    {\n      value,\n      x,\n      y,\n      onValueChange,\n      onGestureStart,\n      onGestureEnd,\n      disabled = false,\n      children,\n      ...rest\n    },\n    forwardedRef,\n  ) {\n    const surfaceRef = useRef<HTMLDivElement | null>(null)\n\n    const valueAtPointer = useCallback(\n      (clientX: number, clientY: number): XYPadPoint | null => {\n        const rect = surfaceRef.current?.getBoundingClientRect()\n        if (!rect) return null\n        // Screen Y grows downward; the pad's Y grows upward. The shared\n        // geometry inverts it, so this pad and PlanPad agree.\n        const at = planeFractionAtPointer(rect, clientX, clientY)\n        if (!at) return null\n        return { x: planeAxisValue(at.x, x), y: planeAxisValue(at.y, y) }\n      },\n      [x.min, x.max, y.min, y.max],\n    )\n\n    const drag = useDrag({\n      values: [value.x, value.y],\n      disabled,\n      onDragStart: (event) => {\n        onGestureStart?.()\n        const next = valueAtPointer(event.clientX, event.clientY)\n        if (next) onValueChange(next)\n      },\n      onDrag: (_dx, _dy, _state, event) => {\n        const next = valueAtPointer(event.clientX, event.clientY)\n        if (next) onValueChange(next)\n      },\n      onDragEnd: () => onGestureEnd?.(),\n    })\n\n    const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n      if (disabled) return\n      // Shift is the fine pass here: one step against the plain arrow's five.\n      const next = planeKeyStep(event.key, event.shiftKey, value, x, y)\n      if (!next) return\n      event.preventDefault()\n      onValueChange(next)\n    }\n\n    return (\n      <div\n        ref={(node) => {\n          surfaceRef.current = node\n          if (typeof forwardedRef === \"function\") forwardedRef(node)\n          else if (forwardedRef) forwardedRef.current = node\n        }}\n        role=\"application\"\n        tabIndex={disabled ? -1 : 0}\n        aria-disabled={disabled || undefined}\n        onPointerDown={drag.onPointerDown}\n        onKeyDown={handleKeyDown}\n        {...rest}\n      >\n        {children({\n          dragging: drag.dragging,\n          x01: normalize(value.x, x.min, x.max),\n          y01: normalize(value.y, y.min, y.max),\n        })}\n      </div>\n    )\n  },\n)\n",
      "type": "registry:component",
      "target": "@ui/xy-pad-headless.tsx"
    }
  ]
}
