# wts-scroll: complete LLM context

> Complete release documentation, package metadata, public TypeScript declarations, published styles, migration guidance, and license for wts-scroll.

This file is generated by `npm run build`. Prefer `llms.txt` when a concise documentation map is sufficient.

<document path="README.md">
# wts-scroll

Dependency-free custom scrollbars for vanilla JavaScript and any browser framework.

Version 3 provides two APIs:

- `WtsScroll`, an imperative TypeScript/JavaScript controller.
- `<wts-scroll>`, a standards-based Web Component.

Both APIs keep native browser scrolling as the source of truth, support mouse, touch, keyboard, and programmatic scrolling, and have no Angular runtime dependency.

Modern scrolling features include bidirectional pagination without visual
jumps, mount/refresh reach evaluation for underfilled feeds, `scroll-end` and
overflow notifications, interaction-aware tracks, position persistence,
element alignment, overscroll containment, native scroll snap, and
primary-axis wheel routing.

## Install

```bash
npm install wts-scroll
```

The published runtime has no dependencies and supports Node.js 18 or newer for
server-side imports. Building, testing, or publishing from this repository uses
newer tooling and requires Node.js `^22.22.2`, `^24.15.0`, or `>=26.0.0`, as
declared in `devEngines`. npm warns when repository commands run on a different
development runtime.

## Required sizing

The mount target or `<wts-scroll>` element must have a definite height. Horizontal scrolling also requires a definite width and content wider than the viewport.

```css
.scroll-host,
wts-scroll {
  display: block;
  width: 100%;
  height: 24rem;
  min-width: 0;
  min-height: 0;
}
```

When the scroller is inside a flex or grid layout, its ancestors may also need `min-height: 0` and `min-width: 0`.

## Controller

The controller moves the mount target's existing children into its native scrolling viewport. `destroy()` removes owned DOM and restores those children to the target.

```html
<div id="feed" class="scroll-host">
  <article>First item</article>
  <article>More content...</article>
</div>
```

```ts
import { WtsScroll } from 'wts-scroll';

const host = document.querySelector('#feed')!;
const scroll = new WtsScroll(host, {
  direction: 'vertical',
  autoHide: true,
  reachEndOffset: '10%',
  minThumbSize: 28,
  onReachEnd(detail) {
    console.log('Load more', detail);
  },
});

host.addEventListener('scroll', (event) => {
  const detail = (event as CustomEvent).detail;
  console.log(detail.progressY);
});

scroll.scrollTo({ top: 240, behavior: 'smooth' });

// When the owning view is removed:
scroll.destroy();
```

A selector, `Element`, or `ShadowRoot` can be used as the target:

```ts
new WtsScroll('#feed');
new WtsScroll(document.querySelector('#feed')!);
new WtsScroll(shadowRoot);
```

## Infinite and bidirectional pagination

Use the existing reach callbacks for both directions. `preservePosition()`
keeps the same content at the same visual position while earlier items are
prepended, including when rendering is asynchronous or item heights vary:

```ts
const scroll = new WtsScroll('#timeline', {
  direction: 'vertical',
  reachStartOffset: '15%',
  reachEndOffset: '15%',
  evaluateReachOnMount: true,
  evaluateReachOnRefresh: true,
  async onReachStart() {
    await scroll.preservePosition(async () => {
      const previous = await loadPreviousPage();
      prependItems(previous);
      await nextRender();
    });
  },
  async onReachEnd() {
    appendItems(await loadNextPage());
  },
});
```

`evaluateReachOnMount` and `evaluateReachOnRefresh` let a short first page
request more content even when it does not yet overflow. Reach events remain
entry-based and rearm after content growth, avoiding repeated requests while
the same threshold stays active.

## Position and navigation

Save and restore a logical, RTL-safe position across tabs, routes, or remounts:

```ts
const state = scroll.saveState();
sessionStorage.setItem('feed-scroll', JSON.stringify(state));

scroll.restoreState(
  JSON.parse(sessionStorage.getItem('feed-scroll')!),
  { mode: 'progress' },
);
```

Align a descendant without calculating offsets:

```ts
scroll.scrollToElement(document.querySelector('#message-42')!, {
  block: 'center',
  inline: 'nearest',
  behavior: 'smooth',
  offset: 12,
});
```

## Modern native behavior

```ts
const scroll = new WtsScroll('#gallery', {
  trackVisibility: 'interaction',
  autoHideDelay: 800,
  overscrollBehavior: 'contain',
  scrollSnap: 'x mandatory',
  wheelAxis: 'primary',
  scrollEndDelay: 120,
});

scroll.root.addEventListener('scroll-end', ({ detail }) => {
  saveProgress(detail);
});

scroll.root.addEventListener('overflow-change', ({ detail }) => {
  console.log(detail.overflowX, detail.overflowY);
});
```

`scroll-end` uses the native event when the browser provides it and a
configurable quiet-period fallback otherwise. Overscroll and snap use the
corresponding CSS standards; unsupported enhancements degrade to ordinary
native scrolling. The core scrollbar and reach behavior do not depend on these
progressive browser features.

## Web Component

Importing `wts-scroll/element` registers `<wts-scroll>` once. The registration is guarded when `customElements` is unavailable.

```ts
import 'wts-scroll/element';
```

```html
<wts-scroll
  class="scroll-host"
  direction="vertical"
  autohide="true"
  track-visibility="interaction"
  auto-hide-delay="800"
  reach-end-offset="10%"
  evaluate-reach-on-mount="true"
  evaluate-reach-on-refresh="true"
  min-thumb-size="28"
  overscroll-behavior="contain"
  scroll-end-delay="120"
  scroll-snap="y proximity"
  wheel-axis="primary"
  aria-label="Notifications"
>
  <article>First notification</article>
  <article>More notifications...</article>
</wts-scroll>

<script type="module">
  const element = document.querySelector('wts-scroll');

  element.addEventListener('reach-end', (event) => {
    console.log('Load more', event.detail);
  });

  element.scrollToEnd({ behavior: 'smooth' });
</script>
```

Use `defineWtsScrollElement()` when registration must be explicit or use a different tag name:

```ts
import { defineWtsScrollElement } from 'wts-scroll/element';

defineWtsScrollElement('app-scroll');
```

### Web Component attributes

| Attribute | Values | Default | Purpose |
| --- | --- | --- | --- |
| `direction` | `both`, `vertical`, `horizontal` | `both` | Enables scrolling and custom tracks by axis. |
| `autohide` | `true`/`false`, `1`/`0` | `true` | Hides tracks whose axes do not overflow. |
| `track-visibility` | `always`, `overflow`, `interaction` | `overflow` | Controls when enabled tracks are visible. |
| `auto-hide-delay` | milliseconds | `800` | Delay before interaction tracks fade. |
| `reach-start-offset` | pixels or percentage | `0` | Start threshold, such as `24`, `24px`, or `10%`. |
| `reach-end-offset` | pixels or percentage | `0` | End threshold, such as `100px` or `15%`. |
| `evaluate-reach-on-mount` | `true`/`false`, `1`/`0` | `false` | Evaluates reach thresholds after mounting, including underfilled content. |
| `evaluate-reach-on-refresh` | `true`/`false`, `1`/`0` | `false` | Evaluates reach thresholds on explicit and observed refreshes. |
| `min-thumb-size` | number | `24` | Minimum thumb size in pixels; values below `8` normalize to the default. |
| `wheel-multiplier` | number | `1` | Multiplies wheel deltas; `1` preserves native wheel handling. |
| `wheel-axis` | `native`, `primary` | `native` | Routes wheel input normally or along the configured primary axis. |
| `overscroll-behavior` | `auto`, `contain`, `none` | `auto` | Controls scroll chaining at viewport boundaries. |
| `scroll-snap` | CSS `scroll-snap-type` value | `none` | Enables native scroll snapping, such as `x mandatory`. |
| `scroll-end-delay` | milliseconds | `120` | Debounce used when native `scrollend` is unavailable. |
| `aria-label` | string | `Scrollable content` | Accessible viewport label. |

The element also exposes `options`, `direction`, `autoHide`,
`trackVisibility`, `overflowState`, `controller`, and `viewport` properties,
plus the controller navigation and state methods.

## Framework usage

`wts-scroll` has no framework adapter. Frameworks use the Web Component and standard DOM events directly.

### React

```tsx
import { createElement, useEffect, useRef } from 'react';
import 'wts-scroll/element';
import type { WtsScrollElement } from 'wts-scroll/element';

export function Feed() {
  const scrollRef = useRef<WtsScrollElement>(null);

  useEffect(() => {
    const element = scrollRef.current;
    const loadMore = () => console.log('Load more');

    element?.addEventListener('reach-end', loadMore);
    return () => element?.removeEventListener('reach-end', loadMore);
  }, []);

  return createElement(
    'wts-scroll',
    {
      ref: scrollRef,
      direction: 'vertical',
      'reach-end-offset': '10%',
      style: { display: 'block', height: '24rem' },
    },
    <div>Scrollable React content</div>,
  );
}
```

### Angular

Import the element registration and allow custom elements in the component schema. No Angular adapter or Angular component is provided.

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'wts-scroll/element';

@Component({
  selector: 'app-feed',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <wts-scroll
      style="display:block;height:24rem"
      direction="vertical"
      reach-end-offset="10%"
      (reach-end)="loadMore()"
    >
      <article>Scrollable Angular content</article>
    </wts-scroll>
  `,
})
export class FeedComponent {
  loadMore(): void {
    console.log('Load more');
  }
}
```

### Vue

Configure Vue's template compiler to recognize the custom element. For
Vue with Vite:

```ts
// vite.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === 'wts-scroll',
        },
      },
    }),
  ],
});
```

```vue
<script setup lang="ts">
import { ref } from 'vue';
import 'wts-scroll/element';
import type { WtsScrollElement } from 'wts-scroll/element';

const scroll = ref<WtsScrollElement>();

function loadMore(): void {
  console.log('Load more');
}
</script>

<template>
  <wts-scroll
    ref="scroll"
    style="display: block; height: 24rem"
    direction="vertical"
    reach-end-offset="10%"
    @reach-end="loadMore"
  >
    <article>Scrollable Vue content</article>
  </wts-scroll>
</template>
```

## Options

`WtsScrollOptions` is accepted by the controller constructor, `setOptions()`, and the Web Component's `options` property.

| Option | Type | Default | Purpose |
| --- | --- | --- | --- |
| `direction` | `'both' \| 'vertical' \| 'horizontal'` | `'both'` | Enables scrolling and tracks by axis. |
| `autoHide` | `boolean` | `true` | Hides a track when its axis does not overflow. |
| `trackVisibility` | `'always' \| 'overflow' \| 'interaction'` | `'overflow'` | Shows tracks continuously, only for overflow, or during interaction. |
| `autoHideDelay` | `number` | `800` | Milliseconds before interaction tracks fade. |
| `reachStartOffset` | `number \| string` | `0` | Distance from the primary-axis start; numbers are pixels and `%` is supported. |
| `reachEndOffset` | `number \| string` | `0` | Distance from the primary-axis end; numbers are pixels and `%` is supported. |
| `evaluateReachOnMount` | `boolean` | `false` | Evaluates thresholds after initial layout, useful for underfilled feeds. |
| `evaluateReachOnRefresh` | `boolean` | `false` | Evaluates thresholds whenever geometry refreshes. |
| `minThumbSize` | `number` | `24` | Minimum thumb size in pixels. |
| `wheelMultiplier` | `number` | `1` | Wheel delta multiplier; `1` keeps native handling. |
| `wheelAxis` | `'native' \| 'primary'` | `'native'` | Uses native wheel axes or routes deltas to the primary axis. |
| `overscrollBehavior` | `'auto' \| 'contain' \| 'none'` | `'auto'` | Controls native overscroll chaining. |
| `scrollSnap` | `string` | `'none'` | Native CSS `scroll-snap-type` value. |
| `scrollEndDelay` | `number` | `120` | Fallback `scroll-end` debounce in milliseconds. |
| `ariaLabel` | `string` | `'Scrollable content'` | Accessible label for the viewport. |
| `injectStyles` | `boolean` | `true` | Injects default controller styles into its mount target. |
| `onScroll` | `(detail: WtsScrollEventDetail) => void \| Promise<void>` | — | Called for native viewport scroll activity. |
| `onReachStart` | `(detail: WtsScrollEventDetail) => void \| Promise<void>` | — | Called when the start threshold becomes active. |
| `onReachEnd` | `(detail: WtsScrollEventDetail) => void \| Promise<void>` | — | Called when the end threshold becomes active. |

Deprecated v2-compatible option aliases are temporarily accepted: `autohide`, `onReachStartOffset`, `onReachEndOffset`, and `speed`. New code should use the canonical v3 names above.

For `direction: 'both'`, the vertical axis is the primary axis used by start/end thresholds. For `direction: 'horizontal'`, the horizontal axis is primary.

## Events

The controller root and Web Component emit bubbling, composed `CustomEvent`s:

| Event | When it fires |
| --- | --- |
| `scroll` | The native viewport scrolls. |
| `scroll-end` | Scrolling settles (native where supported, debounced fallback elsewhere). |
| `reach-start` | The primary axis enters the configured start threshold. |
| `reach-end` | The primary axis enters the configured end threshold. |
| `overflow-change` | Enabled-axis overflow state changes after layout or content updates. |

Reach events fire once on threshold entry. They can fire again after scrolling
outside the threshold and re-entering it. Increasing the primary axis's
scrollable size also rearms both reach callbacks, so appending or prepending a
page cannot leave infinite scrolling latched inside a non-zero threshold.

Callbacks may be synchronous or asynchronous. A callback failure is reported
to the browser without interrupting the controller's remaining scroll and reach
processing.

Each event's `detail` is a `WtsScrollEventDetail`:

```ts
interface WtsScrollEventDetail {
  axis: 'vertical' | 'horizontal';
  top: number;
  left: number;
  maxTop: number;
  maxLeft: number;
  progressX: number;
  progressY: number;
  atStart: boolean;
  atEnd: boolean;
  originalEvent?: Event;
}
```

Horizontal values are logical in right-to-left layouts: `left: 0` is the
inline start (the right edge), and `left: maxLeft` is the inline end. The
controller normalizes browser-specific `viewport.scrollLeft` models for event
details, progress, ARIA values, and its scrolling methods.

`overflow-change` uses `WtsScrollOverflowDetail`, which reports `overflowX`
and `overflowY` together with the current maximum scroll distances.

## Methods and properties

### `WtsScroll`

| Member | Description |
| --- | --- |
| `root` | DOM root owned by the controller. |
| `viewport` | Native scrolling viewport. |
| `content` | Content wrapper inside the native viewport. |
| `currentOptions` | Read-only snapshot of normalized options. |
| `setOptions(options)` | Updates part of the configuration and refreshes geometry. |
| `refresh()` | Recalculates overflow, tracks, thumbs, and reach state. |
| `scrollTo(options)` / `scrollTo(x, y)` | Scrolls to absolute coordinates; horizontal values are logical in RTL. |
| `scrollBy(options)` / `scrollBy(x, y)` | Scrolls by relative coordinates; horizontal values are logical in RTL. |
| `scrollToStart(options?)` | Scrolls the primary axis to its start. |
| `scrollToEnd(options?)` | Scrolls the primary axis to its end. |
| `scrollToElement(element, options?)` | Aligns a descendant with logical block/inline alignment and optional offsets. |
| `saveState()` | Captures logical scroll position and progress for later restoration. |
| `restoreState(state, options?)` | Restores saved state, optionally preferring coordinates or progress. |
| `preservePosition(mutation, options?)` | Runs an async/sync DOM mutation while preserving the visible anchor position. |
| `overflowState` | Current horizontal and vertical overflow snapshot. |
| `destroy()` | Removes listeners/observers, owned DOM, and restores target children. |

`WtsScrollElement` exposes the same scrolling, state, preservation, and refresh
methods, plus `controller`, `viewport`, `options`, `direction`, `autoHide`,
`trackVisibility`, and `overflowState`.

## Optional entry points

The core and Web Component stay dependency-free. Larger or specialized
behaviors are opt-in:

- [`wts-scroll/timeline`](#scroll-timeline) exposes the native
  scroll-driven-animation standard through the internal viewport.
- [`wts-scroll/sync`](#synchronized-scrollers) synchronizes logical positions
  between scrollers without coupling their DOM.
- [`wts-scroll/virtual`](#fixed-size-virtual-list) is a fixed-size,
  render-callback-based virtual list for large collections.

Import only the entry point a view needs; none of them are required by
`WtsScroll` or `<wts-scroll>`.

### Scroll timeline

```ts
import {
  createWtsScrollTimeline,
  supportsWtsScrollTimeline,
} from 'wts-scroll/timeline';

const timeline = createWtsScrollTimeline(scroll, { axis: 'block' });
if (supportsWtsScrollTimeline() && timeline) {
  card.animate(keyframes, { timeline });
}
```

This helper progressively uses the native `ScrollTimeline` API and returns
`null` when it is unavailable or during SSR; it does not install a polyfill.

### Synchronized scrollers

```ts
import { WtsScrollSync } from 'wts-scroll/sync';

const comparison = new WtsScrollSync([leftScroll, rightScroll], {
  axis: 'vertical',
});

// Later:
comparison.destroy();
```

Synchronization uses logical progress, including normalized horizontal RTL
coordinates, so differently sized documents remain aligned.

### Fixed-size virtual list

```ts
import { WtsVirtualScroll } from 'wts-scroll/virtual';

const list = new WtsVirtualScroll('#large-list', {
  items,
  itemSize: 48,
  overscan: 4,
  renderItem(item) {
    const row = document.createElement('article');
    row.textContent = item.title;
    return row;
  },
});
```

Virtual scrolling is deliberately fixed-size and render-callback based. It
does not own framework templates or perform variable-height measurement, which
keeps its behavior deterministic and its core independent of UI frameworks.

## Styling

Default styles are injected automatically. Web Component base styles always remain in its shadow root so native overflow, sizing, and accessibility behavior keep working.

For a controller in light DOM, styles can instead be loaded as a stylesheet:

```ts
import 'wts-scroll/styles.css';

const scroll = new WtsScroll('#feed', {
  injectStyles: false,
});
```

### CSS custom properties

Set variables on the controller mount target or the `<wts-scroll>` element:

| Property | Default |
| --- | --- |
| `--wts-scroll-y-track-background` | translucent `currentColor` |
| `--wts-scroll-y-track-thumb-background` | translucent `currentColor` |
| `--wts-scroll-y-track-thumb-border-radius` | `999px` |
| `--wts-scroll-y-track-width` | `12px` |
| `--wts-scroll-y-track-thumb-width` | `8px` |
| `--wts-scroll-x-track-background` | translucent `currentColor` |
| `--wts-scroll-x-track-thumb-background` | translucent `currentColor` |
| `--wts-scroll-x-track-thumb-border-radius` | `999px` |
| `--wts-scroll-x-track-height` | `12px` |
| `--wts-scroll-x-track-thumb-height` | `8px` |
| `--wts-scroll-track-inset` | `2px` |
| `--wts-scroll-track-opacity` | `1` |
| `--wts-scroll-track-transition` | `opacity 160ms ease` |

```css
wts-scroll {
  --wts-scroll-y-track-width: 10px;
  --wts-scroll-y-track-thumb-width: 6px;
  --wts-scroll-y-track-thumb-background: #5b5bd6;
  --wts-scroll-track-opacity: 0.8;
}
```

### Shadow parts

The Web Component exposes:

| Part | Element |
| --- | --- |
| `viewport` | Native scrolling viewport |
| `content` | Content wrapper |
| `track-y` | Vertical track |
| `thumb-y` | Vertical thumb |
| `track-x` | Horizontal track |
| `thumb-x` | Horizontal thumb |

```css
wts-scroll::part(track-y) {
  border-radius: 999px;
}

wts-scroll::part(thumb-y) {
  box-shadow: 0 0 0 1px rgb(0 0 0 / 20%);
}
```

## TypeScript exports

The root entry exports `WtsScroll`, its option/event/state types (including
`WtsScrollOverflowDetail`), the compatibility `WtsScrollBarOptions` alias, and
`WTS_SCROLL_STYLES`.

The `wts-scroll/element` entry exports `WtsScrollElement`, `WtsScrollElementEventMap`, `WTS_SCROLL_TAG_NAME`, and `defineWtsScrollElement`.
</document>
<document path="MIGRATION.md">
# Migrating from wts-scroll v2 to v3

Version 3 is a framework-agnostic rewrite. It replaces the Angular component with a dependency-free controller and a standards-based Web Component.

There is intentionally no `wts-scroll/angular` adapter. Angular applications use `<wts-scroll>` as a custom element or instantiate `WtsScroll` directly.

## Breaking changes

### Angular package APIs were removed

The following v2 APIs no longer exist:

- `WtsScrollComponent`
- Angular standalone component imports
- The `[wts-scroll]` attribute selector
- Angular `output()` bindings named `onScroll`, `onReachStart`, and `onReachEnd`
- Angular peer dependencies and the ng-packagr output format

The `<wts-scroll>` tag remains, but in v3 it is a browser Custom Element registered by `wts-scroll/element`.

### Registration is explicit

Before:

```ts
import { WtsScrollComponent } from 'wts-scroll';

@Component({
  imports: [WtsScrollComponent],
})
export class FeedComponent {}
```

After:

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'wts-scroll/element';

@Component({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class FeedComponent {}
```

Applications that register custom elements globally can import `wts-scroll/element` once in their browser entry point instead.

### The attribute selector was removed

Before:

```html
<div wts-scroll>Scrollable content</div>
```

After, use the Web Component:

```html
<wts-scroll style="display:block;height:24rem">
  Scrollable content
</wts-scroll>
```

Or use the controller:

```ts
import { WtsScroll } from 'wts-scroll';

const scroll = new WtsScroll('#scroll-host');
```

```html
<div id="scroll-host" style="height:24rem">
  Scrollable content
</div>
```

### Angular outputs became DOM events

Event names now follow DOM conventions.

| v2 Angular output | v3 DOM event | v3 option callback |
| --- | --- | --- |
| `(onScroll)` | `scroll` | `onScroll` |
| `(onReachStart)` | `reach-start` | `onReachStart` |
| `(onReachEnd)` | `reach-end` | `onReachEnd` |

Before:

```html
<wts-scroll (onReachEnd)="loadMore()">
  ...
</wts-scroll>
```

After:

```html
<wts-scroll (reach-end)="loadMore($event)">
  ...
</wts-scroll>
```

Outside Angular templates, use standard event listeners:

```ts
element.addEventListener('reach-end', (event) => {
  const detail = (event as CustomEvent).detail;
  console.log(detail.progressY);
});
```

Events are bubbling, composed `CustomEvent<WtsScrollEventDetail>` instances. Their detail includes the active axis, positions, maximum positions, progress, threshold state, and the original native event when available.

Reach events now fire once when a threshold is entered. They rearm after scrolling outside that threshold.

### Option names changed

Use the canonical v3 names:

| v2 name | v3 name |
| --- | --- |
| `autohide` | `autoHide` |
| `onReachStartOffset` | `reachStartOffset` |
| `onReachEndOffset` | `reachEndOffset` |
| `speed` | `wheelMultiplier` |

The old option spellings remain deprecated aliases on `WtsScrollOptions` for programmatic migration only. HTML attributes use the v3 Web Component names:

```html
<wts-scroll
  autohide="true"
  reach-start-offset="20px"
  reach-end-offset="10%"
  wheel-multiplier="1"
></wts-scroll>
```

New v3 options are:

- `minThumbSize`
- `ariaLabel`
- `injectStyles`
- `onScroll`
- `onReachStart`
- `onReachEnd`

Offsets accept pixel numbers, pixel strings, or percentages. Percentage offsets are calculated from the maximum scroll distance.

### Configuration is now reactive

The v3 controller accepts options during construction and through `setOptions()`:

```ts
const scroll = new WtsScroll('#feed', {
  direction: 'vertical',
  reachEndOffset: '10%',
});

scroll.setOptions({
  autoHide: false,
  minThumbSize: 32,
});
```

The Web Component accepts attributes or properties:

```ts
const element = document.querySelector('wts-scroll')!;

element.direction = 'horizontal';
element.autoHide = false;
element.setOptions({ wheelMultiplier: 1.25 });
```

### Imperative scrolling uses native-style methods

The controller and Web Component expose:

- `scrollTo(options)` and `scrollTo(x, y)`
- `scrollBy(options)` and `scrollBy(x, y)`
- `scrollToStart(options?)`
- `scrollToEnd(options?)`
- `refresh()`
- `setOptions(options)`

The controller also exposes `root`, `viewport`, `currentOptions`, and `destroy()`. The Web Component exposes `controller`, `viewport`, and `options`.

### Host sizing is required

The v3 scroller fills its host. The host must therefore have a definite height:

```css
.scroll-layout,
wts-scroll {
  display: block;
  width: 100%;
  height: 24rem;
  min-width: 0;
  min-height: 0;
}
```

Flex and grid ancestors may also require `min-height: 0` and `min-width: 0`. Without a constrained height, content expands the host and no vertical overflow is created.

### Tracks now overlay the viewport

Version 3 uses native overflow and overlays its custom tracks. It no longer subtracts track dimensions from the content area.

The following theme variables are retained:

- `--wts-scroll-y-track-background`
- `--wts-scroll-y-track-thumb-background`
- `--wts-scroll-y-track-thumb-border-radius`
- `--wts-scroll-y-track-width`
- `--wts-scroll-y-track-thumb-width`
- `--wts-scroll-x-track-background`
- `--wts-scroll-x-track-thumb-background`
- `--wts-scroll-x-track-thumb-border-radius`
- `--wts-scroll-x-track-height`
- `--wts-scroll-x-track-thumb-height`

Internal geometry variables from v2 are no longer supported:

- `--wts-scroll-y-track-thumb-top`
- `--wts-scroll-y-track-thumb-height`
- `--wts-scroll-x-track-thumb-left`
- `--wts-scroll-x-track-thumb-width`

The controller now calculates geometry and writes it directly. New theme variables are `--wts-scroll-track-inset`, `--wts-scroll-track-opacity`, and `--wts-scroll-track-transition`.

The Web Component also exposes `viewport`, `track-y`, `thumb-y`, `track-x`, and `thumb-x` shadow parts:

```css
wts-scroll::part(thumb-y) {
  background: rebeccapurple;
}
```

### Style loading changed

Default styles are injected automatically.

For a controller in light DOM, styles can be managed as a separate import:

```ts
import 'wts-scroll/styles.css';

const scroll = new WtsScroll('#feed', {
  injectStyles: false,
});
```

The Web Component always keeps its base styles inside the shadow root. Customize it through inherited CSS variables and exposed shadow parts.

## Complete Angular migration

Before:

```ts
import { Component } from '@angular/core';
import { WtsScrollComponent } from 'wts-scroll';

@Component({
  selector: 'app-feed',
  standalone: true,
  imports: [WtsScrollComponent],
  template: `
    <wts-scroll (onReachEnd)="loadMore()">
      <article>Content</article>
    </wts-scroll>
  `,
})
export class FeedComponent {
  loadMore(): void {}
}
```

After:

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'wts-scroll/element';

@Component({
  selector: 'app-feed',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <wts-scroll
      style="display:block;height:24rem"
      direction="vertical"
      reach-end-offset="10%"
      (reach-end)="loadMore()"
    >
      <article>Content</article>
    </wts-scroll>
  `,
})
export class FeedComponent {
  loadMore(): void {}
}
```

No Angular adapter is required or shipped.

## Package entry points

| Entry | Purpose |
| --- | --- |
| `wts-scroll` | Controller, options, event types, and style string |
| `wts-scroll/element` | Web Component class and registration |
| `wts-scroll/styles.css` | Standalone default stylesheet |

See [README.md](./README.md) for the full v3 API.
</document>
<document path="package.json">
{
  "name": "wts-scroll",
  "version": "3.0.1",
  "description": "Framework-agnostic scrolling toolkit with custom scrollbars, infinite and virtual scrolling, synchronized views, Web Components, RTL, and accessibility.",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Suman201/wts-scroll-angular-example.git"
  },
  "homepage": "https://github.com/Suman201/wts-scroll-angular-example#readme",
  "bugs": {
    "url": "https://github.com/Suman201/wts-scroll-angular-example/issues"
  },
  "license": "MIT",
  "author": {
    "name": "Suman Mandal"
  },
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  },
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "style": "./dist/styles.css",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./element": {
      "types": "./dist/element.d.ts",
      "import": "./dist/element.js",
      "require": "./dist/element.cjs"
    },
    "./timeline": {
      "types": "./dist/timeline.d.ts",
      "import": "./dist/timeline.js",
      "require": "./dist/timeline.cjs"
    },
    "./sync": {
      "types": "./dist/sync.d.ts",
      "import": "./dist/sync.js",
      "require": "./dist/sync.cjs"
    },
    "./virtual": {
      "types": "./dist/virtual.d.ts",
      "import": "./dist/virtual.js",
      "require": "./dist/virtual.cjs"
    },
    "./styles.css": "./dist/styles.css",
    "./package.json": "./package.json"
  },
  "files": [
    "dist",
    "README.md",
    "MIGRATION.md",
    "llms.txt",
    "llms-full.txt",
    "LICENSE"
  ],
  "sideEffects": [
    "./src/element.ts",
    "./dist/element.js",
    "./dist/element.cjs",
    "./dist/styles.css"
  ],
  "scripts": {
    "build": "node build.mjs",
    "check": "npm test && npm run build && npm run test:package && npm run test:browser",
    "prepack": "npm run build",
    "prepublishOnly": "npm run check",
    "pretest:package": "npm run build",
    "pretest:browser": "npm run build",
    "pretest:browser:ui": "npm run build",
    "test": "vitest run",
    "test:browser": "playwright test",
    "test:browser:install": "playwright install chromium firefox webkit",
    "test:browser:ui": "playwright test --ui",
    "test:package": "node --test test/*.test.mjs",
    "test:watch": "vitest"
  },
  "keywords": [
    "scrollbar",
    "custom-scrollbar",
    "overlay-scrollbar",
    "scrolling",
    "infinite-scroll",
    "virtual-scroll",
    "scroll-sync",
    "scroll-timeline",
    "pagination",
    "web-component",
    "custom-element",
    "framework-agnostic",
    "dependency-free",
    "accessibility",
    "a11y",
    "rtl",
    "horizontal-scroll",
    "vertical-scroll",
    "typescript",
    "javascript",
    "vanilla-js",
    "angular",
    "react",
    "vue",
    "svelte",
    "ssr"
  ],
  "engines": {
    "node": ">=18"
  },
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "^22.22.2 || ^24.15.0 || >=26.0.0",
      "onFail": "warn"
    }
  },
  "devDependencies": {
    "@playwright/test": "1.62.1",
    "esbuild": "^0.28.1",
    "jsdom": "^30.0.1",
    "typescript": "~6.0.3",
    "vitest": "^4.1.10"
  }
}
</document>
<document path="dist/index.d.ts">
export type WtsScrollDirection = 'both' | 'horizontal' | 'vertical';
export type WtsScrollAxis = 'horizontal' | 'vertical';
export type WtsScrollOffset = number | string;
export type WtsScrollTarget = Element | ShadowRoot | string;
export type WtsScrollTrackVisibility = 'always' | 'overflow' | 'interaction';
export type WtsScrollOverscrollBehavior = 'auto' | 'contain' | 'none';
export type WtsScrollWheelAxis = 'native' | 'primary';
export type WtsScrollSnapType = 'none' | 'x mandatory' | 'x proximity' | 'y mandatory' | 'y proximity' | 'both mandatory' | 'both proximity';
export type WtsScrollCallback = (detail: WtsScrollEventDetail) => void | Promise<void>;
export interface WtsScrollOverflowDetail {
    overflowX: boolean;
    overflowY: boolean;
    maxTop: number;
    maxLeft: number;
}
export interface WtsScrollState {
    top: number;
    /** Logical horizontal offset. */
    left: number;
    maxTop: number;
    maxLeft: number;
    progressX: number;
    progressY: number;
}
export interface WtsScrollPreserveOptions {
    axis?: WtsScrollAxis;
    /** Edge at which content is being inserted. Defaults to `start`. */
    edge?: 'start' | 'end';
}
export interface WtsScrollRestoreOptions {
    mode?: 'absolute' | 'progress';
}
export interface WtsScrollToElementOptions extends ScrollIntoViewOptions {
    /** Gap, in pixels, from the selected alignment edge. */
    offset?: number;
}
export type WtsScrollOverflowCallback = (detail: WtsScrollOverflowDetail) => void | Promise<void>;
export interface WtsScrollEventDetail {
    axis: WtsScrollAxis;
    top: number;
    /** Logical horizontal offset: 0 at inline-start and maxLeft at inline-end. */
    left: number;
    maxTop: number;
    maxLeft: number;
    progressX: number;
    progressY: number;
    atStart: boolean;
    atEnd: boolean;
    originalEvent?: Event;
}
export interface WtsScrollOptions {
    /** Axes on which native scrolling and custom tracks are enabled. */
    direction?: WtsScrollDirection;
    /** Hides a track when its axis does not overflow. */
    autoHide?: boolean;
    /** Controls when enabled tracks are visible. Supersedes `autoHide`. */
    trackVisibility?: WtsScrollTrackVisibility;
    /** Delay before interaction-visible tracks are hidden, in milliseconds. */
    autoHideDelay?: number;
    /** Native overscroll chaining behavior applied to the viewport. */
    overscrollBehavior?: WtsScrollOverscrollBehavior;
    /** Distance from the start that activates `reach-start`; numbers are pixels. */
    reachStartOffset?: WtsScrollOffset;
    /** Distance from the end that activates `reach-end`; numbers are pixels. */
    reachEndOffset?: WtsScrollOffset;
    /** Smallest rendered thumb size, in pixels. */
    minThumbSize?: number;
    /** Multiplies wheel deltas. Keep `1` to preserve fully native wheel handling. */
    wheelMultiplier?: number;
    /** Maps a vertical wheel gesture to a horizontal primary scroller. */
    wheelAxis?: WtsScrollWheelAxis;
    /** Native CSS scroll snapping mode applied to the viewport. */
    scrollSnap?: WtsScrollSnapType;
    /** Evaluates reach thresholds once after mounting. */
    evaluateReachOnMount?: boolean;
    /** Evaluates reach thresholds after every explicit/observed refresh. */
    evaluateReachOnRefresh?: boolean;
    /** Fallback delay used when the native `scrollend` event is unavailable. */
    scrollEndDelay?: number;
    /** Accessible label applied to the native scrolling viewport. */
    ariaLabel?: string;
    /** Injects the package's default styles into the mount target. */
    injectStyles?: boolean;
    onScroll?: WtsScrollCallback;
    onReachStart?: WtsScrollCallback;
    onReachEnd?: WtsScrollCallback;
    onScrollEnd?: WtsScrollCallback;
    onOverflowChange?: WtsScrollOverflowCallback;
    /** @deprecated Use `autoHide`. */
    autohide?: boolean;
    /** @deprecated Use `reachStartOffset`. */
    onReachStartOffset?: WtsScrollOffset;
    /** @deprecated Use `reachEndOffset`. */
    onReachEndOffset?: WtsScrollOffset;
    /** @deprecated Use `wheelMultiplier`. */
    speed?: number;
}
/** Backward-compatible name for the former options interface. */
export type WtsScrollBarOptions = WtsScrollOptions;
/**
 * Dependency-free custom scrollbar controller.
 *
 * Existing children of the mount target become the viewport content. `destroy()`
 * restores those children and removes only DOM owned by this instance.
 */
export declare class WtsScroll {
    readonly root: HTMLDivElement;
    readonly viewport: HTMLDivElement;
    readonly content: HTMLDivElement;
    private readonly target;
    private readonly document;
    private readonly ownerWindow;
    private readonly trackY;
    private readonly trackX;
    private readonly thumbY;
    private readonly thumbX;
    private readonly cleanups;
    private readonly pendingFrames;
    private preserveQueue;
    private wheelCleanup?;
    private styleElement?;
    private resizeObserver?;
    private mutationObserver?;
    private options;
    private geometryY;
    private geometryX;
    private drag?;
    private frame?;
    private interactionTimer?;
    private scrollEndTimer?;
    private destroyed;
    private hasMounted;
    private nativeScrollEnd;
    private reachStartActive;
    private reachEndActive;
    private reachMaximum;
    private lastOverflow?;
    constructor(target: WtsScrollTarget, options?: WtsScrollOptions);
    get currentOptions(): Readonly<WtsScrollOptions>;
    setOptions(options: Partial<WtsScrollOptions>): void;
    refresh(): void;
    get overflowState(): Readonly<WtsScrollOverflowDetail>;
    saveState(): WtsScrollState;
    restoreState(state: WtsScrollState, options?: WtsScrollRestoreOptions): void;
    preservePosition(mutation: () => void | Promise<void>, options?: WtsScrollPreserveOptions): Promise<void>;
    private performPreservePosition;
    scrollToElement(element: Element, options?: WtsScrollToElementOptions): void;
    scrollTo(options: ScrollToOptions): void;
    scrollTo(x: number, y: number): void;
    scrollBy(options: ScrollToOptions): void;
    scrollBy(x: number, y: number): void;
    scrollToStart(options?: ScrollOptions): void;
    scrollToEnd(options?: ScrollOptions): void;
    destroy(): void;
    private resolveTarget;
    private createElement;
    private createTrack;
    private createThumb;
    private applyOptions;
    private syncStyles;
    private addListeners;
    private syncWheelListener;
    private listen;
    private startObservers;
    private observeResizeTargets;
    private readonly handleContentChange;
    private queueRefresh;
    private waitForRender;
    private waitForStableGeometry;
    private readonly handleScroll;
    private readonly handleNativeScrollEnd;
    private readonly handleInteraction;
    private readonly handleWheel;
    private readonly handleTrackPointerDown;
    private readonly handleThumbPointerDown;
    private readonly handleThumbPointerMove;
    private readonly handleThumbPointerEnd;
    private finishDrag;
    private readonly handleTrackKeyDown;
    private updateAxis;
    private primaryAxis;
    private isRtl;
    private maxLeft;
    private logicalLeft;
    private rawLeft;
    private capturePreserveAnchors;
    private preserveAnchorDelta;
    private preserveGeometrySample;
    private contentRelativeCoordinate;
    private flattenedContentElements;
    private alignmentDelta;
    private showInteraction;
    private scheduleScrollEnd;
    private emitScrollEnd;
    private emitOverflowChange;
    private clearTimer;
    private snapshot;
    private rearmReachAfterGrowth;
    private invokeCallback;
    private reportCallbackError;
    private evaluateReach;
    private dispatch;
    private assertActive;
}
export { WTS_SCROLL_STYLES } from './styles.js';
</document>
<document path="dist/element.d.ts">
import { WtsScroll, type WtsScrollDirection, type WtsScrollEventDetail, type WtsScrollOptions, type WtsScrollOverflowDetail, type WtsScrollPreserveOptions, type WtsScrollRestoreOptions, type WtsScrollState, type WtsScrollToElementOptions, type WtsScrollTrackVisibility } from './index.js';
declare const HTMLElementBase: typeof HTMLElement;
export declare const WTS_SCROLL_TAG_NAME = "wts-scroll";
export interface WtsScrollElementEventMap {
    scroll: CustomEvent<WtsScrollEventDetail>;
    'scroll-end': CustomEvent<WtsScrollEventDetail>;
    'reach-start': CustomEvent<WtsScrollEventDetail>;
    'reach-end': CustomEvent<WtsScrollEventDetail>;
    'overflow-change': CustomEvent<WtsScrollOverflowDetail>;
}
export declare class WtsScrollElement extends HTMLElementBase {
    static get observedAttributes(): string[];
    private scrollController?;
    private configuredOptions;
    connectedCallback(): void;
    disconnectedCallback(): void;
    attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
    get controller(): WtsScroll | undefined;
    get viewport(): HTMLDivElement | null;
    get options(): Readonly<WtsScrollOptions>;
    set options(value: WtsScrollOptions);
    get direction(): WtsScrollDirection;
    set direction(value: WtsScrollDirection);
    get autoHide(): boolean;
    set autoHide(value: boolean);
    get trackVisibility(): WtsScrollTrackVisibility;
    set trackVisibility(value: WtsScrollTrackVisibility);
    get overflowState(): WtsScrollOverflowDetail | undefined;
    addEventListener<K extends keyof WtsScrollElementEventMap>(type: K, listener: (this: WtsScrollElement, event: WtsScrollElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
    addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, event: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof WtsScrollElementEventMap>(type: K, listener: (this: WtsScrollElement, event: WtsScrollElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
    removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, event: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
    setOptions(options: Partial<WtsScrollOptions>): void;
    refresh(): void;
    scrollTo(options: ScrollToOptions): void;
    scrollTo(x: number, y: number): void;
    scrollBy(options: ScrollToOptions): void;
    scrollBy(x: number, y: number): void;
    scrollToStart(options?: ScrollOptions): void;
    scrollToEnd(options?: ScrollOptions): void;
    saveState(): WtsScrollState;
    restoreState(state: WtsScrollState, options?: WtsScrollRestoreOptions): void;
    preservePosition(mutation: () => void | Promise<void>, options?: WtsScrollPreserveOptions): Promise<void>;
    scrollToElement(element: Element, options?: WtsScrollToElementOptions): void;
    destroy(): void;
    private readOptions;
    private readBooleanAttribute;
}
export declare function defineWtsScrollElement(tagName?: string, registry?: CustomElementRegistry | undefined): void;
declare global {
    interface HTMLElementTagNameMap {
        'wts-scroll': WtsScrollElement;
    }
}
export {};
</document>
<document path="dist/timeline.d.ts">
import type { WtsScroll } from './index.js';
import type { WtsScrollElement } from './element.js';
export type WtsScrollTimelineAxis = 'block' | 'inline' | 'x' | 'y';
/**
 * Options accepted by the native ScrollTimeline constructor.
 *
 * This package intentionally exposes only the interoperable source and axis
 * surface and does not install a polyfill.
 */
export interface WtsScrollTimelineOptions {
    axis?: WtsScrollTimelineAxis;
}
/**
 * Minimal structural type for a native ScrollTimeline. It remains usable in
 * TypeScript configurations whose DOM library does not declare ScrollTimeline.
 */
export interface WtsScrollTimelineLike {
    readonly source?: Element | null;
    readonly axis?: WtsScrollTimelineAxis;
    readonly currentTime?: unknown;
}
export type WtsScrollTimelineSource = WtsScroll | WtsScrollElement | Element;
/** Returns true when the current runtime provides the native ScrollTimeline API. */
export declare function supportsWtsScrollTimeline(): boolean;
/**
 * Creates a native ScrollTimeline using the internal WtsScroll viewport.
 *
 * Returns `null` when ScrollTimeline is unavailable, including during SSR.
 */
export declare function createWtsScrollTimeline(source: WtsScrollTimelineSource, options?: WtsScrollTimelineOptions): WtsScrollTimelineLike | null;
</document>
<document path="dist/sync.d.ts">
import type { WtsScroll } from './index.js';
import type { WtsScrollElement } from './element.js';
export type WtsScrollSyncAxis = 'vertical' | 'horizontal' | 'both';
export type WtsScrollSyncSource = WtsScroll | WtsScrollElement;
export interface WtsScrollSyncOptions {
    /** Axes whose logical progress should be synchronized. */
    axis?: WtsScrollSyncAxis;
}
/**
 * Synchronizes the logical scroll progress of two or more WtsScroll instances.
 *
 * Horizontal synchronization uses WtsScroll's logical coordinates, so inline
 * start remains zero in both LTR and RTL documents.
 */
export declare class WtsScrollSync {
    private readonly records;
    private readonly expected;
    private axis;
    private applying;
    private destroyed;
    constructor(sources: readonly WtsScrollSyncSource[], options?: WtsScrollSyncOptions);
    get size(): number;
    get currentAxis(): WtsScrollSyncAxis;
    setAxis(axis: WtsScrollSyncAxis): void;
    add(source: WtsScrollSyncSource): this;
    remove(source: WtsScrollSyncSource): boolean;
    /** Refreshes all participating controllers after their layout changes. */
    refresh(): void;
    /** Removes synchronization listeners without destroying the controllers. */
    destroy(): void;
    private handleScroll;
    private assertActive;
}
</document>
<document path="dist/virtual.d.ts">
import { WtsScroll, type WtsScrollOptions, type WtsScrollTarget } from './index.js';
export type WtsVirtualScrollAlign = 'start' | 'center' | 'end' | 'nearest';
export type WtsVirtualScrollRenderResult = Node | string | number | null | undefined;
export interface WtsVirtualScrollOptions<T> {
    items: readonly T[];
    /** Fixed height of every item in CSS pixels. */
    itemSize: number;
    renderItem: (item: T, index: number) => WtsVirtualScrollRenderResult;
    /** Extra items rendered before and after the visible range. Defaults to 2. */
    overscan?: number;
    ariaLabel?: string;
    /**
     * Core scrollbar options. Virtual scrolling is currently vertical-only, so
     * direction is intentionally controlled by this class.
     */
    scrollOptions?: Omit<WtsScrollOptions, 'direction' | 'ariaLabel'>;
}
export interface WtsVirtualScrollToIndexOptions {
    align?: WtsVirtualScrollAlign;
    behavior?: ScrollBehavior;
}
/**
 * Dependency-free fixed-size vertical virtual list.
 *
 * The list owns a WtsScroll controller and renders only the visible window plus
 * overscan. Destroying it restores the mount target's original child nodes.
 */
export declare class WtsVirtualScroll<T> {
    readonly controller: WtsScroll;
    private readonly target;
    private readonly originalChildren;
    private readonly spacer;
    private readonly window;
    private readonly itemSize;
    private readonly overscan;
    private readonly renderItem;
    private readonly renderedRows;
    private items;
    private rangeStart;
    private rangeEnd;
    private resizeObserver?;
    private windowResizeCleanup?;
    private destroyed;
    constructor(target: WtsScrollTarget, options: WtsVirtualScrollOptions<T>);
    get length(): number;
    setItems(items: readonly T[]): void;
    scrollToIndex(index: number, options?: WtsVirtualScrollToIndexOptions): void;
    refresh(): void;
    destroy(): void;
    private readonly handleScroll;
    private readonly handleResize;
    private startResizeMonitoring;
    private stopResizeMonitoring;
    private updateSpacer;
    private render;
    private createRow;
    private populateRow;
    private focusedRowIndex;
    private assertActive;
}
</document>
<document path="dist/styles.d.ts">
export declare const WTS_SCROLL_STYLES: string;
</document>
<document path="dist/styles.css">
:host {
  display: block;
  height: 100%;
  min-height: 0;
  min-width: 0;
  position: relative;
  width: 100%;
}

.wts-scroll {
  box-sizing: border-box;
  display: block;
  height: 100%;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  position: relative;
  width: 100%;
}

.wts-scroll *,
.wts-scroll *::before,
.wts-scroll *::after {
  box-sizing: border-box;
}

.wts-scroll__viewport {
  height: 100%;
  min-height: 0;
  min-width: 0;
  overscroll-behavior: auto;
  scrollbar-width: none;
  width: 100%;
}

.wts-scroll__viewport::-webkit-scrollbar {
  display: none;
  height: 0;
  width: 0;
}

.wts-scroll__content {
  min-height: 100%;
  min-width: 100%;
}

.wts-scroll--both > .wts-scroll__viewport {
  overflow: scroll;
}

.wts-scroll--vertical > .wts-scroll__viewport {
  overflow-x: hidden;
  overflow-y: scroll;
}

.wts-scroll--horizontal > .wts-scroll__viewport {
  overflow-x: scroll;
  overflow-y: hidden;
}

.wts-scroll__track {
  align-items: center;
  background: transparent;
  border: 0;
  margin: 0;
  opacity: var(--wts-scroll-track-opacity, 1);
  padding: 0;
  position: absolute;
  touch-action: none;
  transition: var(--wts-scroll-track-transition, opacity 160ms ease);
  z-index: 1;
}

.wts-scroll__track[hidden] {
  display: none;
}

.wts-scroll--track-visibility-interaction > .wts-scroll__track {
  opacity: 0;
  pointer-events: none;
}

.wts-scroll--track-visibility-interaction:hover > .wts-scroll__track,
.wts-scroll--track-visibility-interaction:focus-within > .wts-scroll__track,
.wts-scroll--track-visibility-interaction.wts-scroll--interacting
  > .wts-scroll__track {
  opacity: var(--wts-scroll-track-opacity, 1);
  pointer-events: auto;
}

.wts-scroll__track:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: -2px;
}

.wts-scroll__track--y {
  background: var(--wts-scroll-y-track-background, rgba(127, 127, 127, 0.2));
  bottom: var(--wts-scroll-track-inset, 2px);
  right: var(--wts-scroll-track-inset, 2px);
  top: var(--wts-scroll-track-inset, 2px);
  width: var(--wts-scroll-y-track-width, 12px);
}

.wts-scroll__track--x {
  background: var(--wts-scroll-x-track-background, rgba(127, 127, 127, 0.2));
  bottom: var(--wts-scroll-track-inset, 2px);
  height: var(--wts-scroll-x-track-height, 12px);
  left: var(--wts-scroll-track-inset, 2px);
  right: var(--wts-scroll-track-inset, 2px);
}

.wts-scroll--track-x.wts-scroll--track-y > .wts-scroll__track--y {
  bottom: calc(
    var(--wts-scroll-x-track-height, 12px) +
      var(--wts-scroll-track-inset, 2px)
  );
}

.wts-scroll--track-x.wts-scroll--track-y > .wts-scroll__track--x {
  right: calc(
    var(--wts-scroll-y-track-width, 12px) +
      var(--wts-scroll-track-inset, 2px)
  );
}

.wts-scroll__thumb {
  border: 0;
  cursor: grab;
  margin: 0;
  padding: 0;
  position: absolute;
  touch-action: none;
}

.wts-scroll__thumb:active {
  cursor: grabbing;
}

.wts-scroll__thumb--y {
  background: var(
    --wts-scroll-y-track-thumb-background,
    rgba(96, 96, 96, 0.72)
  );
  border-radius: var(--wts-scroll-y-track-thumb-border-radius, 999px);
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: var(--wts-scroll-y-track-thumb-width, 8px);
}

.wts-scroll__thumb--x {
  background: var(
    --wts-scroll-x-track-thumb-background,
    rgba(96, 96, 96, 0.72)
  );
  border-radius: var(--wts-scroll-x-track-thumb-border-radius, 999px);
  bottom: 50%;
  height: var(--wts-scroll-x-track-thumb-height, 8px);
  left: 0;
  transform: translateY(50%);
}

@supports (background: color-mix(in srgb, currentColor 50%, transparent)) {
  .wts-scroll__track--y {
    background: var(
      --wts-scroll-y-track-background,
      color-mix(in srgb, currentColor 16%, transparent)
    );
  }

  .wts-scroll__track--x {
    background: var(
      --wts-scroll-x-track-background,
      color-mix(in srgb, currentColor 16%, transparent)
    );
  }

  .wts-scroll__thumb--y {
    background: var(
      --wts-scroll-y-track-thumb-background,
      color-mix(in srgb, currentColor 62%, transparent)
    );
  }

  .wts-scroll__thumb--x {
    background: var(
      --wts-scroll-x-track-thumb-background,
      color-mix(in srgb, currentColor 62%, transparent)
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .wts-scroll {
    --wts-scroll-track-transition: none;
  }
}
</document>
<document path="LICENSE">
MIT License

Copyright (c) 2026 Suman Mandal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</document>
