///|
pub(all) enum ResizablePanelSide {
  FirstPanel
  SecondPanel
} derive(Debug, Eq)

///|
priv enum ResizableGroupMsg {
  SetResizableGroupSizes(Array[Int])
  ResizeResizableGroupAt(Int, Int)
}

///|
/// Opaque render scope owned by [`resizable_group`].
struct ResizableGroupScope {
  id : String
  sizes : Array[Int]
  min_sizes : Array[Int]
  max_sizes : Array[Int]
  orientation : SeparatorOrientation
  aria_label : String
  emit : @cmd.Emit[ResizableGroupMsg]
}

///|
/// Opaque compatibility scope owned by [`resizable_panel_group`].
struct ResizableScope {
  group : ResizableGroupScope
  control_id : String
}

///|
priv struct ResizableHandleBounds {
  valid : Bool
  value : Int
  min : Int
  max : Int
}

///|
fn resizable_int_min(left : Int, right : Int) -> Int {
  if left < right {
    left
  } else {
    right
  }
}

///|
fn resizable_int_max(left : Int, right : Int) -> Int {
  if left > right {
    left
  } else {
    right
  }
}

///|
fn resizable_clamp(value : Int, min : Int, max : Int) -> Int {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}

///|
fn resizable_parse(value : String, fallback : Int) -> Int {
  @string.parse_int(value) catch {
    _ => fallback
  }
}

///|
fn resizable_value_at(values : Array[Int], index : Int, fallback : Int) -> Int {
  values.get(index).unwrap_or(fallback)
}

///|
fn resizable_sum(values : Array[Int]) -> Int {
  let mut total = 0
  for value in values {
    total += value
  }
  total
}

///|
fn resizable_prefix_size(values : Array[Int], end : Int) -> Int {
  let mut total = 0
  for index, value in values {
    if index < end {
      total += value
    }
  }
  total
}

///|
fn resizable_pointer_percent(
  orientation : SeparatorOrientation,
  client_x : Int,
  client_y : Int,
  group_left : Int,
  group_top : Int,
  group_width : Int,
  group_height : Int,
) -> Int? {
  let (position, start, length) = if orientation is Vertical {
    (client_y, group_top, group_height)
  } else {
    (client_x, group_left, group_width)
  }
  if length <= 0 {
    return None
  }
  let offset = resizable_clamp(position - start, 0, length)
  Some((offset * 100 + length / 2) / length)
}

///|
fn resizable_normalize_sizes(
  values : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
) -> Array[Int] {
  if values.length() == 0 {
    return [100]
  }
  let sizes : Array[Int] = []
  for index, value in values {
    sizes.push(resizable_clamp(value, min_sizes[index], max_sizes[index]))
  }
  let total = resizable_sum(sizes)
  if total < 100 {
    let mut remaining = 100 - total
    for index, size in sizes {
      if remaining > 0 {
        let capacity = max_sizes[index] - size
        let delta = resizable_int_min(capacity, remaining)
        sizes[index] = size + delta
        remaining -= delta
      }
    }
    // Infeasible max constraints cannot satisfy both policies. Preserve the
    // layout invariant (sum 100) and leave the impossible bound visible in the
    // handle range instead of producing a broken flex layout.
    if remaining > 0 {
      let last = sizes.length() - 1
      sizes[last] += remaining
    }
  } else if total > 100 {
    let mut remaining = total - 100
    for index, size in sizes {
      if remaining > 0 {
        let capacity = size - min_sizes[index]
        let delta = resizable_int_min(capacity, remaining)
        sizes[index] = size - delta
        remaining -= delta
      }
    }
    if remaining > 0 {
      let last = sizes.length() - 1
      sizes[last] -= remaining
    }
  }
  sizes
}

///|
fn resizable_prepare_layout(
  default_sizes : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
) -> (Array[Int], Array[Int], Array[Int]) {
  let defaults = if default_sizes.length() == 0 {
    [100]
  } else {
    default_sizes.copy()
  }
  let minimums : Array[Int] = []
  let maximums : Array[Int] = []
  for index, _ in defaults {
    let minimum = resizable_clamp(
      resizable_value_at(min_sizes, index, 0),
      0,
      100,
    )
    let maximum = resizable_clamp(
      resizable_value_at(max_sizes, index, 100),
      minimum,
      100,
    )
    minimums.push(minimum)
    maximums.push(maximum)
  }
  (resizable_normalize_sizes(defaults, minimums, maximums), minimums, maximums)
}

///|
fn resizable_handle_bounds(
  sizes : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
  between : Int,
) -> ResizableHandleBounds {
  if between < 0 || between + 1 >= sizes.length() {
    return { valid: false, value: 0, min: 0, max: 0 }
  }
  let left = sizes[between]
  let right = sizes[between + 1]
  let pair_total = left + right
  let minimum = resizable_int_max(
    min_sizes[between],
    pair_total - max_sizes[between + 1],
  )
  let maximum = resizable_int_min(
    max_sizes[between],
    pair_total - min_sizes[between + 1],
  )
  if minimum > maximum {
    { valid: false, value: left, min: left, max: left }
  } else {
    { valid: true, value: left, min: minimum, max: maximum }
  }
}

///|
fn resizable_resize_between(
  sizes : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
  between : Int,
  requested : Int,
) -> Array[Int] {
  let next = sizes.copy()
  let bounds = resizable_handle_bounds(sizes, min_sizes, max_sizes, between)
  if !bounds.valid {
    return next
  }
  let pair_total = sizes[between] + sizes[between + 1]
  let left = resizable_clamp(requested, bounds.min, bounds.max)
  next[between] = left
  next[between + 1] = pair_total - left
  next
}

///|
fn resizable_resize_at_percent(
  sizes : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
  between : Int,
  percent : Int,
) -> Array[Int] {
  resizable_resize_between(
    sizes,
    min_sizes,
    max_sizes,
    between,
    percent - resizable_prefix_size(sizes, between),
  )
}

///|
fn resizable_key_size(
  size : Int,
  min : Int,
  max : Int,
  orientation : SeparatorOrientation,
  key : String,
) -> Int? {
  let requested = match (orientation, key) {
    (_, "Home") => Some(min)
    (_, "End") => Some(max)
    (Horizontal, "ArrowLeft") => Some(size - 1)
    (Horizontal, "ArrowRight") => Some(size + 1)
    (Vertical, "ArrowUp") => Some(size - 1)
    (Vertical, "ArrowDown") => Some(size + 1)
    _ => None
  }
  requested.map(next => resizable_clamp(next, min, max))
}

///|
fn resizable_key_for_direction(
  orientation : SeparatorOrientation,
  key : String,
  rtl : Bool,
) -> String {
  if orientation is Horizontal && rtl {
    match key {
      "ArrowLeft" => "ArrowRight"
      "ArrowRight" => "ArrowLeft"
      _ => key
    }
  } else {
    key
  }
}

///|
fn resizable_key_sizes(
  sizes : Array[Int],
  min_sizes : Array[Int],
  max_sizes : Array[Int],
  between : Int,
  orientation : SeparatorOrientation,
  key : String,
) -> Array[Int]? {
  let bounds = resizable_handle_bounds(sizes, min_sizes, max_sizes, between)
  if !bounds.valid {
    return None
  }
  resizable_key_size(bounds.value, bounds.min, bounds.max, orientation, key).map(requested => {
      resizable_resize_between(sizes, min_sizes, max_sizes, between, requested)
    },
  )
}

///|
fn resizable_is_arrow_key(key : String) -> Bool {
  key == "ArrowLeft" ||
  key == "ArrowRight" ||
  key == "ArrowUp" ||
  key == "ArrowDown"
}

///|
#cfg(target="js")
priv struct ResizablePointerActive {
  pointer_id : Int
  between : Int
  handle : @dom.Element
}

///|
#cfg(target="js")
priv struct ResizablePointerBinding {
  root : @dom.Element
  mut resize : (Int, Int, Int, Int, Int, Int, Int) -> Unit
  mut active : ResizablePointerActive?
}

///|
#cfg(target="js")
let resizable_pointer_bindings : Array[ResizablePointerBinding] = []

///|
#cfg(target="js")
fn resizable_prune_pointer_bindings() -> Unit {
  let mut index = 0
  while index < resizable_pointer_bindings.length() {
    if resizable_pointer_bindings[index].root.get_is_connected() {
      index += 1
    } else {
      ignore(resizable_pointer_bindings.remove(index))
    }
  }
}

///|
#cfg(target="js")
fn resizable_pointer_binding(root : @dom.Element) -> ResizablePointerBinding? {
  for binding in resizable_pointer_bindings {
    if binding.root.is_same_node(root.as_node()) {
      return Some(binding)
    }
  }
  None
}

///|
#cfg(target="js")
fn resizable_update_pointer(
  binding : ResizablePointerBinding,
  pointer : @dom.PointerEvent,
) -> Unit {
  guard binding.active is Some(active) else { return }
  guard active.pointer_id == pointer.get_pointer_id() else { return }
  let rect = binding.root.get_bounding_client_rect()
  let client_x = if binding.root.get_attribute("data-orientation").unwrap_or("") ==
    "horizontal" &&
    ui_element_is_rtl(binding.root) {
    rect.get_left() +
    rect.get_width() -
    (pointer.get_client_x().to_double() - rect.get_left())
  } else {
    pointer.get_client_x().to_double()
  }
  (binding.resize)(
    active.between,
    client_x.round().to_int(),
    pointer.get_client_y(),
    rect.get_left().round().to_int(),
    rect.get_top().round().to_int(),
    rect.get_width().round().to_int(),
    rect.get_height().round().to_int(),
  )
}

///|
#cfg(target="js")
fn resizable_finish_pointer(
  binding : ResizablePointerBinding,
  pointer : @dom.PointerEvent,
) -> Unit {
  guard binding.active is Some(active) else { return }
  guard active.pointer_id == pointer.get_pointer_id() else { return }
  active.handle.remove_attribute("data-resizing")
  binding.active = None
  if binding.root.has_pointer_capture(active.pointer_id) {
    binding.root.release_pointer_capture(active.pointer_id)
  }
}

///|
#cfg(target="js")
fn resizable_bind_pointer_listeners(binding : ResizablePointerBinding) -> Unit {
  binding.root.add_event_listener("pointerdown", event => {
    guard event.to_pointer_event() is Some(pointer) else { return }
    if pointer.get_button() != 0 || binding.active is Some(_) {
      return
    }
    guard pointer.target().to_element() is Some(target) else { return }
    guard target.closest("[data-slot=\"resizable-handle\"][data-between]")
      is Some(handle) else {
      return
    }
    guard binding.root.contains(handle.as_node()) else { return }
    guard handle.closest("[data-slot=\"resizable-panel-group\"]") is Some(owner) &&
      owner.is_same_node(binding.root.as_node()) else {
      return
    }
    guard handle.query_selector("[data-slot=\"resizable-control\"]")
      is Some(control) else {
      return
    }
    if !ui_element_is_enabled(control) {
      return
    }
    let between = ui_parse_int_or(
      handle.get_attribute("data-between").unwrap_or(""),
      -1,
    )
    if between < 0 {
      return
    }
    binding.active = Some({
      pointer_id: pointer.get_pointer_id(),
      between,
      handle,
    })
    handle.set_attribute("data-resizing", "true")
    ui_focus_element_without_scroll(control)
    pointer.prevent_default()
    binding.root.set_pointer_capture(pointer.get_pointer_id())
    resizable_update_pointer(binding, pointer)
  })
  binding.root.add_event_listener("pointermove", event => {
    guard event.to_pointer_event() is Some(pointer) else { return }
    if binding.active is Some(_) {
      pointer.prevent_default()
      resizable_update_pointer(binding, pointer)
    }
  })
  let finish : @dom.Listener = event => {
    if event.to_pointer_event() is Some(pointer) {
      resizable_finish_pointer(binding, pointer)
    }
  }
  binding.root.add_event_listener("pointerup", finish)
  binding.root.add_event_listener("pointercancel", finish)
  binding.root.add_event_listener("lostpointercapture", finish)
}

///|
#cfg(target="js")
fn bind_resizable_pointer(
  id : String,
  resize : (Int, Int, Int, Int, Int, Int, Int) -> Unit,
) -> Unit {
  guard ui_element_by_id(id) is Some(root) else { return }
  resizable_prune_pointer_bindings()
  if resizable_pointer_binding(root) is Some(binding) {
    binding.resize = resize
    return
  }
  let binding : ResizablePointerBinding = { root, resize, active: None }
  resizable_pointer_bindings.push(binding)
  resizable_bind_pointer_listeners(binding)
}

///|
#cfg(target="js")
fn resizable_event_is_rtl(event : @dom.KeyboardEvent) -> Bool {
  event.current_target().to_option() is Some(current_target) &&
  current_target.to_element() is Some(target) &&
  target.closest("[data-slot=\"resizable-panel-group\"]") is Some(group) &&
  ui_element_is_rtl(group)
}

///|
#cfg(target="js")
fn resizable_bind_pointer_cmd(
  id : String,
  orientation : SeparatorOrientation,
  emit : @cmd.Emit[ResizableGroupMsg],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    ui_after_mount(
      id,
      () => {
        bind_resizable_pointer(id, (
          between,
          client_x,
          client_y,
          left,
          top,
          width,
          height,
        ) => {
          if resizable_pointer_percent(
              orientation, client_x, client_y, left, top, width, height,
            )
            is Some(percent) {
            scheduler.add(emit(ResizeResizableGroupAt(between, percent)))
          }
        })
      },
      purpose="resizable-pointer-bind",
    )
  })
}

///|
#cfg(target="js")
fn resizable_group_input_attrs(
  scope : ResizableGroupScope,
  between : Int,
  label : String,
) -> @html.Attrs {
  let attrs = @html.Attrs::build()
    .data_set("slot", "resizable-control")
    .aria_label(label)
    .aria_orientation(separator_orientation_value(scope.orientation))
  ignore(
    attrs.on_keydown(event => {
      let key = resizable_key_for_direction(
        scope.orientation,
        event.key(),
        resizable_event_is_rtl(event),
      )
      let next = resizable_key_sizes(
        scope.sizes,
        scope.min_sizes,
        scope.max_sizes,
        between,
        scope.orientation,
        key,
      )
      if next is Some(next) {
        event.prevent_default()
        if next == scope.sizes {
          @cmd.none
        } else {
          (scope.emit)(SetResizableGroupSizes(next))
        }
      } else if resizable_is_arrow_key(key) {
        // A native range also handles orthogonal arrow keys. Suppress them so
        // its keyboard axis remains identical to the visible panel-group axis.
        event.prevent_default()
        @cmd.none
      } else {
        @cmd.none
      }
    }),
  )
  attrs
}

///|
#cfg(not(target="js"))
fn resizable_group_input_attrs(
  scope : ResizableGroupScope,
  between : Int,
  label : String,
) -> @html.Attrs {
  ignore(between)
  @html.Attrs::build()
  .data_set("slot", "resizable-control")
  .aria_label(label)
  .aria_orientation(separator_orientation_value(scope.orientation))
}

///|
fn[C : @html.IsChildren] render_resizable_group(
  scope : ResizableGroupScope,
  class : String?,
  title : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  children : (ResizableGroupScope) -> C,
) -> @html.Html {
  let orientation_value = separator_orientation_value(scope.orientation)
  let first_size = resizable_value_at(scope.sizes, 0, 100)
  @html.div(
    id=scope.id,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "resizable-panel-group")
      .data_set("orientation", orientation_value)
      .data_set("size", "\{first_size}")
      .data_set("sizes", scope.sizes.map(size => "\{size}").join(","))
      .data_set("panel-count", "\{scope.sizes.length()}"),
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        "display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden",
        if scope.orientation is Vertical {
          "flex-direction:column"
        } else {
          "flex-direction:row"
        },
      ],
      style,
    ),
    children(scope),
  )
}

///|
#cfg(target="js")
pub fn[C : @html.IsChildren] resizable_group(
  id~ : String,
  default_sizes~ : Array[Int],
  min_sizes? : Array[Int] = [],
  max_sizes? : Array[Int] = [],
  orientation? : SeparatorOrientation = Horizontal,
  aria_label? : String = "Resize panels",
  on_resize? : @cmd.Emit[Array[Int]],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (ResizableGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
  let (initial, minimums, maximums) = resizable_prepare_layout(
    default_sizes, min_sizes, max_sizes,
  )
  let (sizes, emit) = @rabbita.create_state_with_init(
    init=emit => (initial, resizable_bind_pointer_cmd(id, orientation, emit)),
    update=fn(_, msg, current) {
      let next = match msg {
        SetResizableGroupSizes(sizes) => sizes
        ResizeResizableGroupAt(between, percent) =>
          resizable_resize_at_percent(
            current, minimums, maximums, between, percent,
          )
      }
      let command = if next != current && on_resize is Some(notify) {
        notify(next.copy())
      } else {
        @cmd.none
      }
      (next, command)
    },
  )
  sizes.view(sizes => {
    render_resizable_group(
      {
        id,
        sizes,
        min_sizes: minimums,
        max_sizes: maximums,
        orientation,
        aria_label,
        emit,
      },
      class,
      title,
      attrs,
      style,
      children,
    )
  })
}

///|
#cfg(not(target="js"))
pub fn[C : @html.IsChildren] resizable_group(
  id~ : String,
  default_sizes~ : Array[Int],
  min_sizes? : Array[Int] = [],
  max_sizes? : Array[Int] = [],
  orientation? : SeparatorOrientation = Horizontal,
  aria_label? : String = "Resize panels",
  on_resize? : @cmd.Emit[Array[Int]],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (ResizableGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
  ignore(on_resize)
  let (sizes, minimums, maximums) = resizable_prepare_layout(
    default_sizes, min_sizes, max_sizes,
  )
  let emit = @cmd.Emit(_ => @cmd.none)
  @rabbita.Val::constant(
    render_resizable_group(
      {
        id,
        sizes,
        min_sizes: minimums,
        max_sizes: maximums,
        orientation,
        aria_label,
        emit,
      },
      class,
      title,
      attrs,
      style,
      children,
    ),
  )
}

///|
pub fn[C : @html.IsChildren] resizable_group_panel(
  scope~ : ResizableGroupScope,
  index~ : Int,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let size = resizable_value_at(scope.sizes, index, 0)
  let panel_id = id.unwrap_or("\{scope.id}-panel-\{index}")
  @html.div(
    id=panel_id,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "resizable-panel")
      .data_set("index", "\{index}")
      .data_set("size", "\{size}"),
    style=ui_styles(
      [UiBoxSizing, "min-width:0;min-height:0;overflow:auto;flex:0 0 \{size}%"],
      style,
    ),
    children,
  )
}

///|
fn render_resizable_handle(
  scope : ResizableGroupScope,
  between : Int,
  outer_id : String?,
  control_id : String,
  with_handle : Bool,
  aria_label : String?,
  class : String?,
  title : String?,
  attrs : @html.Attrs?,
  style : Array[String],
) -> @html.Html {
  let vertical = scope.orientation is Vertical
  let bounds = resizable_handle_bounds(
    scope.sizes,
    scope.min_sizes,
    scope.max_sizes,
    between,
  )
  let label = aria_label.unwrap_or(scope.aria_label)
  let input_attrs = resizable_group_input_attrs(scope, between, label)
  if !bounds.valid {
    ignore(input_attrs.disabled(true))
  }
  let id = outer_id
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .role("separator")
      .aria_orientation(if vertical { "horizontal" } else { "vertical" })
      .aria_valuemin("\{bounds.min}")
      .aria_valuemax("\{bounds.max}")
      .aria_valuenow("\{bounds.value}")
      .data_set("slot", "resizable-handle")
      .data_set("orientation", if vertical { "horizontal" } else { "vertical" })
      .data_set("between", "\{between}"),
    style=ui_styles(
      [
        "position:relative;z-index:1;display:flex;flex-shrink:0;align-items:center;justify-content:center;outline:none",
        "touch-action:none;user-select:none",
        if vertical {
          "width:100%;height:0;cursor:row-resize"
        } else {
          "width:0;height:100%;cursor:col-resize"
        },
      ],
      style,
    ),
    [
      @html.span(
        attrs=@html.Attrs::build()
          .data_set("slot", "resizable-handle-line")
          .aria_hidden("true"),
        style=[
          "position:absolute;z-index:0;display:block;background:var(--rui-border,oklch(0.922 0 0));pointer-events:none",
          if vertical {
            "left:0;top:-0.5px;width:100%;height:1px"
          } else {
            "left:-0.5px;top:0;width:1px;height:100%"
          },
        ],
        "",
      ),
      @html.span(
        attrs=@html.Attrs::build()
          .data_set("slot", "resizable-handle-hit-area")
          .aria_hidden("true"),
        style=[
          "position:absolute;z-index:2;display:block",
          if vertical {
            "left:0;top:-2px;width:100%;height:4px;cursor:row-resize"
          } else {
            "left:-2px;top:0;width:4px;height:100%;cursor:col-resize"
          },
        ],
        "",
      ),
      if with_handle {
        @html.span(
          attrs=@html.Attrs::build()
            .data_set("slot", "resizable-handle-grip")
            .aria_hidden("true"),
          style=[
            "position:relative;z-index:1;display:block;width:0.25rem;height:1.5rem;flex-shrink:0;border-radius:9999px;background:var(--rui-border,oklch(0.922 0 0));pointer-events:none",
            if vertical {
              "transform:rotate(90deg)"
            } else {
              ""
            },
          ],
          "",
        )
      } else {
        @html.nothing
      },
      @html.input(
        input_type=@html.Range,
        value="\{bounds.value}",
        min=bounds.min,
        max=bounds.max,
        step=1,
        id=control_id,
        on_input=scope.emit.map(text => {
          SetResizableGroupSizes(
            resizable_resize_between(
              scope.sizes,
              scope.min_sizes,
              scope.max_sizes,
              between,
              resizable_parse(text, bounds.value),
            ),
          )
        }),
        attrs=input_attrs,
        style=[
          "position:absolute;z-index:2;margin:0;opacity:0;pointer-events:none",
          if vertical {
            "left:0;top:-2px;width:100%;height:4px;writing-mode:vertical-lr;cursor:row-resize"
          } else {
            "left:-2px;top:0;width:4px;height:100%;writing-mode:horizontal-tb;cursor:col-resize"
          },
        ],
      ),
    ],
  )
}

///|
pub fn resizable_group_handle(
  scope~ : ResizableGroupScope,
  between~ : Int,
  with_handle? : Bool = false,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let handle_id = "\{scope.id}-handle-\{between}"
  render_resizable_handle(
    scope,
    between,
    Some(handle_id),
    handle_id + "-input",
    with_handle,
    aria_label,
    class,
    title,
    attrs,
    style,
  )
}

///|
pub fn[C : @html.IsChildren] resizable_panel_group(
  id~ : String,
  default_size? : Int = 50,
  min? : Int = 10,
  max? : Int = 90,
  orientation? : SeparatorOrientation = Horizontal,
  aria_label? : String = "Resize panels",
  on_resize? : @cmd.Emit[Int],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (ResizableScope) -> C,
) -> @rabbita.Val[@html.Html] {
  let min = resizable_clamp(min, 0, 100)
  let max = resizable_clamp(max, min, 100)
  let initial = resizable_clamp(default_size, min, max)
  let on_resize = if on_resize is Some(notify) {
    Some(notify.map(sizes => resizable_value_at(sizes, 0, initial)))
  } else {
    None
  }
  resizable_group(
    id~,
    default_sizes=[initial, 100 - initial],
    min_sizes=[min, 100 - max],
    max_sizes=[max, 100 - min],
    orientation~,
    aria_label~,
    on_resize?,
    class?,
    title?,
    attrs?,
    style~,
    group => children({ group, control_id: "\{id}-handle" }),
  )
}

///|
pub fn[C : @html.IsChildren] resizable_panel(
  scope~ : ResizableScope,
  side~ : ResizablePanelSide,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let side_value = if side is FirstPanel { "first" } else { "second" }
  let index = if side is FirstPanel { 0 } else { 1 }
  let size = resizable_value_at(scope.group.sizes, index, 0)
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "resizable-panel")
      .data_set("side", side_value)
      .data_set("size", "\{size}"),
    style=ui_styles(
      [UiBoxSizing, "min-width:0;min-height:0;overflow:auto;flex:0 0 \{size}%"],
      style,
    ),
    children,
  )
}

///|
pub fn resizable_handle(
  scope~ : ResizableScope,
  with_handle? : Bool = false,
  aria_label? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  render_resizable_handle(
    scope.group,
    0,
    id,
    scope.control_id,
    with_handle,
    aria_label,
    class,
    title,
    attrs,
    style,
  )
}