{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-resize-handle",
  "type": "registry:ui",
  "title": "Resize Handle",
  "description": "The splitter between two stacked or side-by-side panes, controlling one pane's size in pixels as a focusable separator with aria-valuenow. Drag with pointer capture (live onValueChange, one onValueCommitted on release or a lost capture), step it with the arrow keys, Shift or Page Up/Down for a large step, Home and End for the bounds, and collapse or restore the pane with Enter or a double-click. The key and drag maths sit in a pure core beside it.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/resize-handle.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/resize-handle\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 * ResizeHandle — the splitter between two panes, controlling one pane's size\n * in pixels: the code editor above, the card palette below.\n *\n * It is a focusable `role=\"separator\"` carrying the size as its value\n * (`aria-valuenow`, 0 while collapsed), so a screen reader can read and change\n * it:\n *\n *   - Drag it (the pointer is captured, so a fast drag cannot slip off). The\n *     size follows live through `onValueChange` and is committed once on\n *     release through `onValueCommitted`. A drag also ends, and commits, when\n *     the capture is lost (a window blur, an element removed under it), so\n *     the handle is never left dragging.\n *   - Arrow keys along its axis move it by `step`, Shift+arrow and Page\n *     Up/Down by `largeStep`; Home and End jump to `min` and `max`. Each key\n *     press both changes and commits.\n *   - Enter or a double-click collapses or restores the pane, when the\n *     consumer handles `onCollapsedChange`; dragging a collapsed pane, or a\n *     key that grows it, expands it again.\n *\n * `orientation=\"horizontal\"` is a horizontal bar between a top and a bottom\n * pane (dragged up and down); \"vertical\" is a vertical bar between side-by-side\n * panes. `direction` says which pane it controls: -1 (the default) for the\n * pane below or right of the handle, which shrinks as the handle moves\n * towards it; +1 for the pane above or left. The key and drag rules live in\n * `resize-handle-math.ts`.\n *\n * Drawn as a thin grip line in an 8px hit area, with the resize cursor and\n * `touch-action: none` so a touch drag does not scroll the page.\n */\n\nimport {\n  useRef,\n  useState,\n  type CSSProperties,\n  type JSX,\n  type KeyboardEvent,\n  type PointerEvent,\n} from \"react\"\nimport { ConsoleStyles } from \"maruaudio/ui/shared/console-styles\"\nimport {\n  clampPaneSize,\n  resizeAriaValue,\n  resizeDragValue,\n  resizeKeyAction,\n  type ResizeDirection,\n  type ResizeOrientation,\n} from \"maruaudio/ui/shared/resize-handle-math\"\n\nexport interface ResizeHandleProps {\n  /** The controlled pane's size, px. */\n  readonly value: number\n  readonly min: number\n  readonly max: number\n  /** Live while dragging, and per key press. */\n  readonly onValueChange: (value: number) => void\n  /** At the end of a drag, and per key press. */\n  readonly onValueCommitted?: (value: number) => void\n  /** \"horizontal\": a horizontal bar between top and bottom panes (drag vertically). Default \"horizontal\". */\n  readonly orientation?: ResizeOrientation\n  /** +1 when moving the pointer down/right grows the pane, -1 when it shrinks it (a pane below or right of the handle). Default -1. */\n  readonly direction?: ResizeDirection\n  readonly collapsed?: boolean\n  /** Enter or double-click toggles. */\n  readonly onCollapsedChange?: (collapsed: boolean) => void\n  /** Arrow key step, px. Defaults to 16. */\n  readonly step?: number\n  /** Shift+arrow and Page Up/Down step, px. Defaults to 64. */\n  readonly largeStep?: number\n  readonly \"aria-label\": string\n  /** The id of the pane this handle sizes. */\n  readonly \"aria-controls\"?: string\n  readonly className?: string\n  readonly style?: CSSProperties\n}\n\n/** The hit area across the handle, px. */\nconst HIT = 8\n/** Pointer travel, px, before a press on a collapsed pane counts as a drag. */\nconst DRAG_THRESHOLD = 3\n\ninterface Drag {\n  readonly pointerId: number\n  readonly startPointer: number\n  readonly startValue: number\n  moved: boolean\n  last: number\n}\n\nexport function ResizeHandle({\n  value,\n  min,\n  max,\n  onValueChange,\n  onValueCommitted,\n  orientation = \"horizontal\",\n  direction = -1,\n  collapsed = false,\n  onCollapsedChange,\n  step = 16,\n  largeStep = 64,\n  \"aria-label\": ariaLabel,\n  \"aria-controls\": ariaControls,\n  className,\n  style,\n}: ResizeHandleProps): JSX.Element {\n  const drag = useRef<Drag | null>(null)\n  const [dragging, setDragging] = useState(false)\n  const horizontal = orientation === \"horizontal\"\n  const bounds = { min, max }\n  const along = (event: PointerEvent<HTMLDivElement>) =>\n    horizontal ? event.clientY : event.clientX\n\n  const set = (next: number, commit: boolean) => {\n    onValueChange(next)\n    if (commit) onValueCommitted?.(next)\n  }\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Enter\") {\n      if (!onCollapsedChange) return\n      event.preventDefault()\n      onCollapsedChange(!collapsed)\n      return\n    }\n    const action = resizeKeyAction(event.key, event.shiftKey, {\n      value: clampPaneSize(value, bounds),\n      min,\n      max,\n      orientation,\n      direction,\n      collapsed,\n      step,\n      largeStep,\n    })\n    if (!action) return\n    event.preventDefault()\n    if (action.collapsed === false) onCollapsedChange?.(false)\n    if (action.value !== undefined) set(action.value, true)\n  }\n\n  const endDrag = (event: PointerEvent<HTMLDivElement>) => {\n    const current = drag.current\n    if (!current || current.pointerId !== event.pointerId) return\n    drag.current = null\n    setDragging(false)\n    try {\n      if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n        event.currentTarget.releasePointerCapture(event.pointerId)\n      }\n    } catch {\n      // Already released.\n    }\n    if (current.moved) onValueCommitted?.(current.last)\n  }\n\n  return (\n    <div\n      role=\"separator\"\n      tabIndex={0}\n      aria-orientation={orientation}\n      aria-valuenow={resizeAriaValue(value, collapsed, bounds)}\n      aria-valuemin={Math.min(min, max)}\n      aria-valuemax={Math.max(min, max)}\n      aria-valuetext={collapsed ? \"Collapsed\" : undefined}\n      aria-label={ariaLabel}\n      aria-controls={ariaControls}\n      data-resize-handle=\"\"\n      data-orientation={orientation}\n      data-collapsed={collapsed ? \"\" : undefined}\n      data-dragging={dragging ? \"\" : undefined}\n      className={className}\n      onKeyDown={onKeyDown}\n      onDoubleClick={() => onCollapsedChange?.(!collapsed)}\n      onPointerDown={(event) => {\n        if (event.button !== 0) return\n        event.preventDefault()\n        event.currentTarget.focus()\n        try {\n          event.currentTarget.setPointerCapture?.(event.pointerId)\n        } catch {\n          // A synthetic pointer cannot be captured; moves still arrive here.\n        }\n        const start = collapsed ? 0 : clampPaneSize(value, bounds)\n        drag.current = {\n          pointerId: event.pointerId,\n          startPointer: along(event),\n          startValue: start,\n          moved: false,\n          last: start,\n        }\n        setDragging(true)\n      }}\n      onPointerMove={(event) => {\n        const current = drag.current\n        if (!current || current.pointerId !== event.pointerId) return\n        const pointer = along(event)\n        if (\n          !current.moved &&\n          Math.abs(pointer - current.startPointer) < DRAG_THRESHOLD\n        )\n          return\n        const next = resizeDragValue(\n          current.startValue,\n          current.startPointer,\n          pointer,\n          direction,\n          bounds\n        )\n        if (!current.moved) {\n          current.moved = true\n          if (collapsed) onCollapsedChange?.(false)\n        }\n        if (next === current.last) return\n        current.last = next\n        set(next, false)\n      }}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onLostPointerCapture={endDrag}\n      style={{\n        boxSizing: \"border-box\",\n        position: \"relative\",\n        flex: \"none\",\n        alignSelf: \"stretch\",\n        ...(horizontal\n          ? { height: HIT, width: \"100%\" }\n          : { width: HIT, height: \"100%\" }),\n        cursor: horizontal ? \"row-resize\" : \"col-resize\",\n        touchAction: \"none\",\n        userSelect: \"none\",\n        ...style,\n      }}\n    >\n      <ConsoleStyles />\n      <span\n        aria-hidden=\"true\"\n        data-resize-grip=\"\"\n        style={{\n          position: \"absolute\",\n          pointerEvents: \"none\",\n          borderRadius: 1,\n          ...(horizontal\n            ? {\n                left: 0,\n                right: 0,\n                top: \"50%\",\n                height: dragging ? 2 : 1,\n                transform: \"translateY(-50%)\",\n              }\n            : {\n                top: 0,\n                bottom: 0,\n                left: \"50%\",\n                width: dragging ? 2 : 1,\n                transform: \"translateX(-50%)\",\n              }),\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/resize-handle.tsx"
    }
  ]
}
