{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "resizable",
  "title": "Resizable",
  "type": "registry:ui",
  "description": "Drag-to-resize panel layout — horizontal or vertical splits with persistent sizes. Use for IDE-style sidebars, split views, and any layout the user should be able to reshape.",
  "categories": [
    "layout"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/resizable/resizable.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { GripVertical } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\nexport type ResizableDirection = 'horizontal' | 'vertical'\n\n/**\n * <uip-resizable-panel> — Individual resizable panel.\n */\nexport class UipResizablePanel extends LitElement {\n  static styles = [tailwind, css`:host { display: block; overflow: hidden; min-width: 0; min-height: 0; }`]\n\n  static properties = {\n    defaultSize: { type: Number, attribute: 'default-size' },\n    minSize: { type: Number, attribute: 'min-size' },\n    maxSize: { type: Number, attribute: 'max-size' },\n    size: { type: Number },\n  }\n\n  defaultSize = 50\n  minSize = 0\n  maxSize = 100\n  size = 50\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'resizable-panel')\n    if (this.defaultSize !== undefined) {\n      this.size = this.defaultSize\n    }\n    this.applySize()\n  }\n\n  applySize() {\n    this.style.flex = `${this.size} 1 0px`\n  }\n\n  setSize(newSize: number) {\n    const clamped = Math.min(this.maxSize, Math.max(this.minSize, newSize))\n    this.size = clamped\n    this.applySize()\n  }\n\n  render() {\n    return html`<slot></slot>`\n  }\n}\n\n/**\n * <uip-resizable-handle> — Draggable separator handle between panels.\n */\nexport class UipResizableHandle extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        position: relative;\n        touch-action: none;\n        user-select: none;\n        -webkit-user-select: none;\n      }\n      :host([data-panel-group-direction=\"horizontal\"]) {\n        cursor: col-resize;\n        width: 1px;\n      }\n      :host([data-panel-group-direction=\"vertical\"]) {\n        cursor: row-resize;\n        height: 1px;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    withHandle: { type: Boolean, attribute: 'with-handle' },\n  }\n\n  withHandle = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'resizable-handle')\n    this.setAttribute('tabindex', '0')\n    this.setAttribute('role', 'separator')\n    this.addEventListener('pointerdown', this.onPointerDown.bind(this))\n  }\n\n  private onPointerDown(e: PointerEvent) {\n    if (e.button !== 0) return\n    e.preventDefault()\n    const group = this.closest('uip-resizable-panel-group') as UipResizablePanelGroup | null\n    if (!group) return\n\n    const prevPanel = this.previousElementSibling as UipResizablePanel | null\n    const nextPanel = this.nextElementSibling as UipResizablePanel | null\n    if (!prevPanel || !nextPanel || !(prevPanel instanceof UipResizablePanel) || !(nextPanel instanceof UipResizablePanel)) {\n      return\n    }\n\n    this.setPointerCapture(e.pointerId)\n    const direction = group.direction\n    const startX = e.clientX\n    const startY = e.clientY\n    const startPrevSize = prevPanel.size\n    const startNextSize = nextPanel.size\n    const totalSize = startPrevSize + startNextSize\n\n    const groupRect = group.getBoundingClientRect()\n    const groupPixels = direction === 'horizontal' ? groupRect.width : groupRect.height\n\n    const onPointerMove = (moveEv: PointerEvent) => {\n      const deltaPx = direction === 'horizontal' ? moveEv.clientX - startX : moveEv.clientY - startY\n      const deltaPercent = (deltaPx / groupPixels) * 100\n\n      let newPrev = startPrevSize + deltaPercent\n      let newNext = startNextSize - deltaPercent\n\n      if (newPrev < prevPanel.minSize) {\n        newPrev = prevPanel.minSize\n        newNext = totalSize - newPrev\n      } else if (newPrev > prevPanel.maxSize) {\n        newPrev = prevPanel.maxSize\n        newNext = totalSize - newPrev\n      }\n\n      if (newNext < nextPanel.minSize) {\n        newNext = nextPanel.minSize\n        newPrev = totalSize - newNext\n      } else if (newNext > nextPanel.maxSize) {\n        newNext = nextPanel.maxSize\n        newPrev = totalSize - newNext\n      }\n\n      prevPanel.setSize(newPrev)\n      nextPanel.setSize(newNext)\n    }\n\n    const onPointerUp = (upEv: PointerEvent) => {\n      try {\n        this.releasePointerCapture(upEv.pointerId)\n      } catch {\n        // ignore\n      }\n      this.removeEventListener('pointermove', onPointerMove)\n      this.removeEventListener('pointerup', onPointerUp)\n      this.removeEventListener('pointercancel', onPointerUp)\n    }\n\n    this.addEventListener('pointermove', onPointerMove)\n    this.addEventListener('pointerup', onPointerUp)\n    this.addEventListener('pointercancel', onPointerUp)\n  }\n\n  render() {\n    const group = this.closest('uip-resizable-panel-group') as UipResizablePanelGroup | null\n    const dir = group?.direction ?? 'horizontal'\n    this.setAttribute('data-panel-group-direction', dir)\n\n    return html`\n      <div\n        part=\"handle\"\n        data-uipkge=\"\"\n        data-slot=\"resizable-handle\"\n        data-panel-group-direction=${dir}\n        class=${cn(\n          'bg-border focus-visible:ring-ring relative flex items-center justify-center focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden',\n          dir === 'horizontal'\n            ? 'w-px h-full after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2'\n            : 'h-px w-full after:absolute after:inset-x-0 after:top-1/2 after:h-1 after:-translate-y-1/2 [&>div]:rotate-90',\n        )}\n      >\n        ${this.withHandle\n          ? html`\n              <div class=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n                <slot>${icon(GripVertical, 'grip-vertical', 'size-2.5')}</slot>\n              </div>\n            `\n          : nothing}\n      </div>\n    `\n  }\n}\n\n/**\n * <uip-resizable-panel-group> — Container for resizable panels.\n */\nexport class UipResizablePanelGroup extends LitElement {\n  static styles = [tailwind, css`:host { display: block; width: 100%; height: 100%; }`]\n\n  static properties = {\n    direction: { type: String, reflect: true },\n  }\n\n  direction: ResizableDirection = 'horizontal'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'resizable-panel-group')\n    this.setAttribute('data-panel-group-direction', this.direction)\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (changed.has('direction')) {\n      this.setAttribute('data-panel-group-direction', this.direction)\n      const handles = this.querySelectorAll('uip-resizable-handle')\n      handles.forEach((h) => h.setAttribute('data-panel-group-direction', this.direction))\n    }\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        data-uipkge=\"\"\n        data-slot=\"resizable-panel-group\"\n        data-panel-group-direction=${this.direction}\n        class=${cn(\n          'flex h-full w-full',\n          this.direction === 'vertical' ? 'flex-col' : 'flex-row',\n        )}\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-resizable-panel') || customElements.define('uip-resizable-panel', UipResizablePanel)\ncustomElements.get('uip-resizable-handle') || customElements.define('uip-resizable-handle', UipResizableHandle)\ncustomElements.get('uip-resizable-panel-group') || customElements.define('uip-resizable-panel-group', UipResizablePanelGroup)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-resizable-panel': UipResizablePanel\n    'uip-resizable-handle': UipResizableHandle\n    'uip-resizable-panel-group': UipResizablePanelGroup\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/resizable/resizable.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
