///|
pub fn[C : @html.IsChildren] scroll_area(
  orientation? : SeparatorOrientation = Vertical,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let value = separator_orientation_value(orientation)
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "scroll-area")
      .data_set("orientation", value)
      .tabindex(0),
    style=ui_styles(
      [
        UiBoxSizing,
        "position:relative;width:100%;height:100%;border-radius:inherit;outline-offset:2px;overscroll-behavior:contain;scrollbar-color:var(--rui-border,oklch(0.922 0 0)) transparent;scrollbar-width:thin",
        if orientation is Vertical {
          "overflow-y:auto;overflow-x:hidden"
        } else {
          "overflow-x:auto;overflow-y:hidden"
        },
      ],
      style,
    ),
    children,
  )
}

///|
/// Optional visual scrollbar track for layouts that want shadcn's composition.
/// Native scrolling remains on `scroll_area`; this element is decorative.
pub fn scroll_bar(
  orientation? : SeparatorOrientation = Vertical,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let value = separator_orientation_value(orientation)
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "scroll-area-scrollbar")
      .data_set("orientation", value)
      .aria_hidden("true"),
    style=ui_styles(
      [
        "display:flex;pointer-events:none;user-select:none;padding:1px",
        if orientation is Vertical {
          "position:absolute;top:0;inset-inline-end:0;width:0.625rem;height:100%"
        } else {
          "position:absolute;inset-inline-start:0;bottom:0;width:100%;height:0.625rem;flex-direction:column"
        },
      ],
      style,
    ),
    @html.div(
      attrs=@html.Attrs::build().data_set("slot", "scroll-area-thumb"),
      style=[
        "position:relative;flex:1;border-radius:9999px;background:var(--rui-border,oklch(0.922 0 0));opacity:0.7",
      ],
      "",
    ),
  )
}

///|
priv struct ScrollAreaMetrics {
  offset : Double
  viewport : Double
  content : Double
}

///|
#cfg(target="js")
impl Eq for ScrollAreaMetrics with fn equal(a, b) {
  a.offset == b.offset && a.viewport == b.viewport && a.content == b.content
}

///|
#cfg(target="js")
priv enum ScrollAreaMsg {
  ScrollAreaMeasured(ScrollAreaMetrics)
}

///|
const ScrollAreaManagedRootStyle : String = "position:relative;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:inherit"

///|
const ScrollAreaManagedViewportStyle : String = "width:100%;height:100%;min-width:0;min-height:0;overscroll-behavior:contain;scrollbar-color:transparent transparent;scrollbar-width:none;-ms-overflow-style:none;outline-offset:2px"

///|
const ScrollAreaManagedTrackStyle : String = "position:absolute;z-index:1;display:block;user-select:none;transition:opacity 150ms ease"

///|
const ScrollAreaManagedThumbStyle : String = "position:absolute;display:block;border-radius:9999px;background:var(--rui-border,oklch(0.922 0 0));opacity:0.8;pointer-events:auto;touch-action:none"

///|
#cfg(target="js")
priv struct ScrollAreaDimensions {
  vertical : Bool
  viewport_size : Double
  content_size : Double
  max : Double
}

///|
#cfg(target="js")
priv struct ScrollAreaDrag {
  pointer_id : Int
  grab : Double
}

///|
#cfg(target="js")
priv struct ScrollAreaPointerBinding {
  root : @dom.Element
  viewport : @dom.Element
  track : @dom.Element
  thumb : @dom.Element
  mut vertical : Bool
  mut notify : (Double, Double, Double) -> Unit
  mut drag : ScrollAreaDrag?
}

///|
#cfg(target="js")
let scroll_area_pointer_bindings : Array[ScrollAreaPointerBinding] = []

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

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

///|
#cfg(target="js")
fn scroll_area_dimensions(
  binding : ScrollAreaPointerBinding,
) -> ScrollAreaDimensions {
  let viewport_size = if binding.vertical {
    binding.viewport.get_client_height()
  } else {
    binding.viewport.get_client_width()
  }
  let content_size = if binding.vertical {
    binding.viewport.get_scroll_height()
  } else {
    binding.viewport.get_scroll_width()
  }
  {
    vertical: binding.vertical,
    viewport_size,
    content_size,
    max: (content_size - viewport_size).max(0.0),
  }
}

///|
#cfg(target="js")
fn scroll_area_physical_offset(
  binding : ScrollAreaPointerBinding,
  dimensions : ScrollAreaDimensions,
) -> Double {
  if dimensions.vertical {
    binding.viewport.get_scroll_top().clamp(min=0.0, max=dimensions.max)
  } else {
    let raw = binding.viewport.get_scroll_left()
    (if ui_element_is_rtl(binding.viewport) {
      dimensions.max + raw
    } else {
      raw
    }).clamp(min=0.0, max=dimensions.max)
  }
}

///|
#cfg(target="js")
fn scroll_area_notify_position(binding : ScrollAreaPointerBinding) -> Unit {
  let dimensions = scroll_area_dimensions(binding)
  (binding.notify)(
    scroll_area_physical_offset(binding, dimensions),
    dimensions.viewport_size,
    dimensions.content_size,
  )
}

///|
#cfg(target="js")
fn scroll_area_set_physical_offset(
  binding : ScrollAreaPointerBinding,
  value : Double,
  dimensions : ScrollAreaDimensions,
) -> Unit {
  let next = value.clamp(min=0.0, max=dimensions.max)
  if dimensions.vertical {
    binding.viewport.set_scroll_top(next)
  } else if ui_element_is_rtl(binding.viewport) {
    binding.viewport.set_scroll_left(next - dimensions.max)
  } else {
    binding.viewport.set_scroll_left(next)
  }
  (binding.notify)(next, dimensions.viewport_size, dimensions.content_size)
}

///|
#cfg(target="js")
fn scroll_area_move_pointer(
  binding : ScrollAreaPointerBinding,
  pointer : @dom.PointerEvent,
) -> Unit {
  guard binding.drag is Some(drag) else { return }
  guard drag.pointer_id == pointer.get_pointer_id() else { return }
  let dimensions = scroll_area_dimensions(binding)
  let rect = binding.track.get_bounding_client_rect()
  let track_size = if dimensions.vertical {
    rect.get_height()
  } else {
    rect.get_width()
  }
  let thumb_size = if dimensions.content_size > 0.0 {
    (track_size * dimensions.viewport_size / dimensions.content_size).max(1.0)
  } else {
    track_size
  }
  let travel = (track_size - thumb_size).max(0.0)
  let pointer_position = if dimensions.vertical {
    pointer.get_client_y().to_double() - rect.get_top()
  } else {
    pointer.get_client_x().to_double() - rect.get_left()
  }
  let start = (pointer_position - drag.grab).clamp(min=0.0, max=travel)
  scroll_area_set_physical_offset(
    binding,
    if travel > 0.0 {
      start / travel * dimensions.max
    } else {
      0.0
    },
    dimensions,
  )
  pointer.prevent_default()
}

///|
#cfg(target="js")
fn scroll_area_finish_pointer(
  binding : ScrollAreaPointerBinding,
  pointer : @dom.PointerEvent,
) -> Unit {
  guard binding.drag is Some(drag) else { return }
  guard drag.pointer_id == pointer.get_pointer_id() else { return }
  binding.drag = None
  if binding.track.has_pointer_capture(drag.pointer_id) {
    binding.track.release_pointer_capture(drag.pointer_id)
  }
}

///|
#cfg(target="js")
fn scroll_area_bind_pointer_listeners(
  binding : ScrollAreaPointerBinding,
) -> Unit {
  binding.track.add_event_listener("pointerdown", event => {
    guard event.to_pointer_event() is Some(pointer) else { return }
    if pointer.get_button() != 0 || binding.drag is Some(_) {
      return
    }
    let dimensions = scroll_area_dimensions(binding)
    if dimensions.max <= 0.0 {
      return
    }
    let rect = binding.track.get_bounding_client_rect()
    let track_size = if dimensions.vertical {
      rect.get_height()
    } else {
      rect.get_width()
    }
    let thumb_size = (track_size *
    dimensions.viewport_size /
    dimensions.content_size).max(1.0)
    let travel = (track_size - thumb_size).max(0.0)
    let current_start = scroll_area_physical_offset(binding, dimensions) /
      dimensions.max *
      travel
    let pointer_position = if dimensions.vertical {
      pointer.get_client_y().to_double() - rect.get_top()
    } else {
      pointer.get_client_x().to_double() - rect.get_left()
    }
    let on_thumb = pointer.target().to_element() is Some(target) &&
      target.is_same_node(binding.thumb.as_node())
    binding.drag = Some({
      pointer_id: pointer.get_pointer_id(),
      grab: if on_thumb {
        pointer_position - current_start
      } else {
        thumb_size / 2.0
      },
    })
    binding.track.set_pointer_capture(pointer.get_pointer_id())
    scroll_area_move_pointer(binding, pointer)
  })
  binding.track.add_event_listener("pointermove", event => {
    if event.to_pointer_event() is Some(pointer) {
      scroll_area_move_pointer(binding, pointer)
    }
  })
  let finish : @dom.Listener = event => {
    if event.to_pointer_event() is Some(pointer) {
      scroll_area_finish_pointer(binding, pointer)
    }
  }
  binding.track.add_event_listener("pointerup", finish)
  binding.track.add_event_listener("pointercancel", finish)
  binding.track.add_event_listener("lostpointercapture", finish)
  binding.viewport.add_event_listener_with_options(
    "scroll",
    _ => scroll_area_notify_position(binding),
    passive=true,
  )
}

///|
#cfg(target="js")
fn bind_scroll_area_pointer(
  id : String,
  vertical : Bool,
  notify : (Double, Double, Double) -> Unit,
) -> Unit {
  guard ui_element_by_id(id) is Some(root) else { return }
  scroll_area_prune_pointer_bindings()
  guard root.query_selector("[data-slot=\"scroll-area-viewport\"]")
    is Some(viewport) else {
    return
  }
  guard root.query_selector("[data-slot=\"scroll-area-scrollbar\"]")
    is Some(track) else {
    return
  }
  guard root.query_selector("[data-slot=\"scroll-area-thumb\"]") is Some(thumb) else {
    return
  }
  if scroll_area_pointer_binding(root) is Some(binding) {
    binding.vertical = vertical
    binding.notify = notify
    scroll_area_notify_position(binding)
    return
  }
  let binding : ScrollAreaPointerBinding = {
    root,
    viewport,
    track,
    thumb,
    vertical,
    notify,
    drag: None,
  }
  scroll_area_pointer_bindings.push(binding)
  scroll_area_bind_pointer_listeners(binding)
  scroll_area_notify_position(binding)
}

///|
fn scroll_area_initial_metrics() -> ScrollAreaMetrics {
  { offset: 0.0, viewport: 0.0, content: 0.0 }
}

///|
fn scroll_area_max_offset(metrics : ScrollAreaMetrics) -> Double {
  if metrics.content > metrics.viewport {
    metrics.content - metrics.viewport
  } else {
    0.0
  }
}

///|
fn scroll_area_is_scrollable(metrics : ScrollAreaMetrics) -> Bool {
  scroll_area_max_offset(metrics) > 1.0
}

///|
fn scroll_area_thumb_size_hundredths(metrics : ScrollAreaMetrics) -> Int {
  if !scroll_area_is_scrollable(metrics) || metrics.content <= 0.0 {
    10000
  } else {
    (metrics.viewport / metrics.content * 10000.0)
    .to_int()
    .clamp(min=100, max=10000)
  }
}

///|
fn scroll_area_thumb_offset_hundredths(metrics : ScrollAreaMetrics) -> Int {
  let max_offset = scroll_area_max_offset(metrics)
  if max_offset <= 0.0 {
    0
  } else {
    let available = 10000 - scroll_area_thumb_size_hundredths(metrics)
    (metrics.offset.clamp(min=0.0, max=max_offset) /
    max_offset *
    available.to_double())
    .to_int()
    .clamp(min=0, max=available)
  }
}

///|
fn scroll_area_percent(value : Int) -> String {
  let clamped = value.clamp(min=0, max=10000)
  let whole = clamped / 100
  let fraction = clamped % 100
  if fraction == 0 {
    "\{whole}%"
  } else if fraction < 10 {
    "\{whole}.0\{fraction}%"
  } else {
    "\{whole}.\{fraction}%"
  }
}

///|
#cfg(target="js")
fn scroll_area_initialize_cmd(
  root_id : String,
  viewport_id : String,
  orientation : SeparatorOrientation,
  emit : @cmd.Emit[ScrollAreaMsg],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    ui_after_mount(
      viewport_id,
      () => {
        bind_scroll_area_pointer(root_id, orientation is Vertical, (
          offset,
          viewport,
          content,
        ) => {
          scheduler.add(emit(ScrollAreaMeasured({ offset, viewport, content })))
        })
      },
      purpose="scroll-area-initialize",
    )
  })
}

///|
#cfg(target="js")
fn scroll_area_viewport_attrs(
  orientation : SeparatorOrientation,
  emit : @cmd.Emit[ScrollAreaMsg],
) -> @html.Attrs {
  ignore(orientation)
  ignore(emit)
  @html.Attrs::build()
}

///|
#cfg(target="js")
fn scroll_area_track_attrs(
  viewport_id : String,
  orientation : SeparatorOrientation,
  emit : @cmd.Emit[ScrollAreaMsg],
) -> @html.Attrs {
  ignore(viewport_id)
  ignore(orientation)
  ignore(emit)
  @html.Attrs::build()
}

///|
fn[C : @html.IsChildren] render_scroll_area_with_scrollbar(
  metrics : ScrollAreaMetrics,
  orientation : SeparatorOrientation,
  id : String,
  class : String?,
  title : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  viewport_attrs : @html.Attrs,
  track_attrs : @html.Attrs,
  children : C,
) -> @html.Html {
  let value = separator_orientation_value(orientation)
  let scrollable = scroll_area_is_scrollable(metrics)
  let state = if scrollable { "scrollable" } else { "idle" }
  let size = scroll_area_percent(scroll_area_thumb_size_hundredths(metrics))
  let offset = scroll_area_percent(scroll_area_thumb_offset_hundredths(metrics))
  let viewport_id = "\{id}-viewport"
  @html.div(
    id~,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "scroll-area")
      .data_set("managed", "")
      .data_set("orientation", value)
      .data_set("state", state)
      .data_set("scrollable", ui_bool(scrollable)),
    style=ui_styles([UiBoxSizing, ScrollAreaManagedRootStyle], style),
    [
      @html.div(
        id=viewport_id,
        attrs=viewport_attrs
          .data_set("slot", "scroll-area-viewport")
          .data_set("orientation", value)
          .data_set("state", state)
          .data_set("scroll-offset", "\{metrics.offset}")
          .data_set("viewport-size", "\{metrics.viewport}")
          .data_set("content-size", "\{metrics.content}")
          .tabindex(0),
        style=[
          UiBoxSizing,
          ScrollAreaManagedViewportStyle,
          if orientation is Vertical {
            "overflow-y:auto;overflow-x:hidden"
          } else {
            "overflow-x:auto;overflow-y:hidden"
          },
        ],
        children,
      ),
      @html.div(
        id="\{id}-scrollbar",
        attrs=track_attrs
          .data_set("slot", "scroll-area-scrollbar")
          .data_set("orientation", value)
          .data_set("state", state)
          .data_set("thumb-size", size)
          .data_set("thumb-offset", offset)
          .aria_hidden("true"),
        style=[
          UiBoxSizing,
          ScrollAreaManagedTrackStyle,
          if orientation is Vertical {
            "top:0;inset-inline-end:0;width:var(--rui-scrollbar-size,0.625rem);height:100%"
          } else {
            "inset-inline-start:0;bottom:0;width:100%;height:var(--rui-scrollbar-size,0.625rem)"
          },
          if scrollable {
            "opacity:1;pointer-events:auto;cursor:pointer"
          } else {
            "opacity:0;pointer-events:none"
          },
        ],
        @html.div(
          id="\{id}-thumb",
          attrs=@html.Attrs::build()
            .data_set("slot", "scroll-area-thumb")
            .data_set("orientation", value)
            .data_set("state", state),
          style=[
            UiBoxSizing,
            ScrollAreaManagedThumbStyle,
            if orientation is Vertical {
              "top:\{offset};inset-inline-start:1px;width:calc(100% - 2px);height:\{size}"
            } else {
              "top:1px;left:\{offset};width:\{size};height:calc(100% - 2px)"
            },
          ],
          "",
        ),
      ),
    ],
  )
}

///|
/// A black-box incremental Scroll Area with a proportional custom scrollbar.
///
/// The component measures its viewport after the first render, owns the latest
/// offset/viewport/content metrics in Rabbita state, and refreshes them on each
/// native scroll event. Wheel, touch, and keyboard scrolling remain native.
/// Pointer presses on the track jump the thumb to that position; pressing the
/// thumb preserves its grab offset and drags with pointer capture until release
/// or cancellation. Horizontal RTL scrolling is normalized before it enters
/// component state, so the thumb geometry stays logical across browser modes.
#cfg(target="js")
pub fn[C : @html.IsChildren] scroll_area_with_scrollbar(
  orientation? : SeparatorOrientation = Vertical,
  id~ : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @rabbita.Val[@html.Html] {
  let viewport_id = "\{id}-viewport"
  let (metrics, emit) = @rabbita.create_state_with_init(
    init=emit => {
      (
        scroll_area_initial_metrics(),
        scroll_area_initialize_cmd(id, viewport_id, orientation, emit),
      )
    },
    update=(_, message, _) => {
      (
        match message {
          ScrollAreaMeasured(metrics) => metrics
        },
        @cmd.none,
      )
    },
  )
  metrics.view(metrics => {
    render_scroll_area_with_scrollbar(
      metrics,
      orientation,
      id,
      class,
      title,
      attrs,
      style,
      scroll_area_viewport_attrs(orientation, emit),
      scroll_area_track_attrs(viewport_id, orientation, emit),
      children,
    )
  })
}

///|
#cfg(not(target="js"))
pub fn[C : @html.IsChildren] scroll_area_with_scrollbar(
  orientation? : SeparatorOrientation = Vertical,
  id~ : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @rabbita.Val[@html.Html] {
  @rabbita.Val::constant(
    render_scroll_area_with_scrollbar(
      scroll_area_initial_metrics(),
      orientation,
      id,
      class,
      title,
      attrs,
      style,
      @html.Attrs::build(),
      @html.Attrs::build(),
      children,
    ),
  )
}