# Qwik Virtualizer

A production-ready virtual scrolling component for Qwik applications. This implementation provides efficient rendering of large lists and grids with full feature parity to other framework implementations.

## Installation

```bash
npm install virtua
# or
yarn add virtua
# or
pnpm add virtua
```

Make sure you have Qwik installed:

```bash
npm install @builder.io/qwik
```

## Basic Usage

```tsx
import { component$ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const MyList = component$(() => {
  const items = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
  
  return (
    <div style={{ height: "400px" }}>
      <Virtualizer
        children={items.map((item, index) => (
          <div key={index} style={{ padding: "10px", borderBottom: "1px solid #eee" }}>
            {item}
          </div>
        ))}
      />
    </div>
  );
});
```

## Render Function Pattern

For better performance with large datasets, use the render function pattern:

```tsx
import { component$ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const DynamicList = component$(() => {
  return (
    <div style={{ height: "400px" }}>
      <Virtualizer
        count={10000}
        children={(index: number) => (
          <div key={index} style={{ padding: "10px", borderBottom: "1px solid #eee" }}>
            Dynamic Item {index}
          </div>
        )}
      />
    </div>
  );
});
```

## Imperative API

Access the virtualizer's methods using a signal ref:

```tsx
import { component$, useSignal, $ } from "@builder.io/qwik";
import { Virtualizer, type VirtualizerHandle } from "virtua/qwik";

export const ControlledList = component$(() => {
  const virtualizerRef = useSignal<VirtualizerHandle>();

  const scrollToTop = $(() => {
    virtualizerRef.value?.scrollTo(0);
  });

  const scrollToIndex = $((index: number) => {
    virtualizerRef.value?.scrollToIndex(index);
  });

  return (
    <div>
      <button onClick$={scrollToTop}>Scroll to Top</button>
      <button onClick$={() => scrollToIndex(500)}>Scroll to Middle</button>
      
      <div style={{ height: "400px" }}>
        <Virtualizer
          ref={virtualizerRef}
          count={1000}
          children={(index: number) => (
            <div key={index} style={{ padding: "10px" }}>
              Item {index}
            </div>
          )}
        />
      </div>
    </div>
  );
});
```

## Horizontal Scrolling

```tsx
import { component$ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const HorizontalList = component$(() => {
  return (
    <div style={{ width: "100%", height: "200px" }}>
      <Virtualizer
        count={100}
        horizontal
        children={(index: number) => (
          <div 
            key={index}
            style={{ 
              padding: "20px", 
              minWidth: "150px",
              height: "100%",
              borderRight: "1px solid #eee"
            }}
          >
            Column {index}
          </div>
        )}
      />
    </div>
  );
});
```

## Variable Item Sizes

```tsx
import { component$ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const VariableSizeList = component$(() => {
  const getItemHeight = (index: number) => {
    return index % 3 === 0 ? 80 : index % 2 === 0 ? 60 : 40;
  };

  return (
    <div style={{ height: "400px" }}>
      <Virtualizer
        count={1000}
        itemSize={getItemHeight}
        children={(index: number) => {
          const height = getItemHeight(index);
          return (
            <div 
              key={index}
              style={{ 
                padding: "10px", 
                height: `${height - 1}px`,
                borderBottom: "1px solid #eee"
              }}
            >
              Variable Item {index} (height: {height}px)
            </div>
          );
        }}
      />
    </div>
  );
});
```

## Scroll Event Handling

```tsx
import { component$, useStore, $ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const ScrollEventList = component$(() => {
  const scrollInfo = useStore({
    offset: 0,
    isScrolling: false,
  });

  const handleScroll = $((offset: number) => {
    scrollInfo.offset = offset;
    scrollInfo.isScrolling = true;
  });

  const handleScrollEnd = $(() => {
    scrollInfo.isScrolling = false;
  });

  return (
    <div>
      <div>Scroll Offset: {Math.round(scrollInfo.offset)}px</div>
      
      <div style={{ height: "400px" }}>
        <Virtualizer
          count={1000}
          onScroll$={handleScroll}
          onScrollEnd$={handleScrollEnd}
          children={(index: number) => (
            <div key={index} style={{ padding: "10px" }}>
              Item {index}
            </div>
          )}
        />
      </div>
    </div>
  );
});
```

## Custom Components

Use custom container and item components:

```tsx
import { component$ } from "@builder.io/qwik";
import { Virtualizer } from "virtua/qwik";

export const CustomComponentList = component$(() => {
  const CustomContainer = component$<any>((props) => (
    <ul {...props} style={{ ...props.style, listStyle: "none", margin: 0, padding: 0 }} />
  ));

  const CustomItem = component$<any>((props) => (
    <li {...props} style={{ ...props.style, borderLeft: "4px solid #2196f3" }} />
  ));

  return (
    <div style={{ height: "400px" }}>
      <Virtualizer
        count={1000}
        as={CustomContainer}
        item={CustomItem}
        children={(index: number) => (
          <div key={index} style={{ padding: "10px" }}>
            Custom Item {index}
          </div>
        )}
      />
    </div>
  );
});
```

## Props

### VirtualizerProps

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `JSXNode \| ((index: number) => JSXNode)` | - | Elements to render. Can be an array or render function. |
| `count` | `number` | - | Total number of items (required when using render function). |
| `overscan` | `number` | `4` | Number of items to render outside visible area. |
| `itemSize` | `ItemSize` | - | Item size hint for unmeasured items. |
| `shift` | `boolean` | `false` | Maintain scroll position from end when items change. |
| `horizontal` | `boolean` | `false` | Enable horizontal scrolling. |
| `keepMounted` | `number[]` | - | Indexes that should always stay mounted. |
| `cache` | `CacheSnapshot` | - | Cache to restore scroll position. |
| `startMargin` | `number` | `0` | Margin before the first item. |
| `ssrCount` | `number` | - | Number of items to render during SSR. |
| `as` | `string \| Component` | `"div"` | Container element type. |
| `item` | `string \| Component` | `"div"` | Item wrapper element type. |
| `scrollRef` | `Signal<Element>` | - | Reference to scrollable element. |
| `onScroll$` | `QRL<(offset: number) => void>` | - | Scroll event handler. |
| `onScrollEnd$` | `QRL<() => void>` | - | Scroll end event handler. |
| `ref` | `Signal<VirtualizerHandle>` | - | Reference to virtualizer handle. |

### VirtualizerHandle

| Property/Method | Type | Description |
|-----------------|------|-------------|
| `cache` | `CacheSnapshot` | Current cache snapshot. |
| `scrollOffset` | `number` | Current scroll offset. |
| `scrollSize` | `number` | Total scrollable size. |
| `viewportSize` | `number` | Viewport size. |
| `findStartIndex()` | `() => number` | Find start index of visible range. |
| `findEndIndex()` | `() => number` | Find end index of visible range. |
| `getItemOffset(index)` | `(index: number) => number` | Get item offset. |
| `getItemSize(index)` | `(index: number) => number` | Get item size. |
| `scrollToIndex(index, opts?)` | `(index: number, opts?: ScrollToIndexOpts) => void` | Scroll to specific index. |
| `scrollTo(offset)` | `(offset: number) => void` | Scroll to specific offset. |
| `scrollBy(offset)` | `(offset: number) => void` | Scroll by relative offset. |

## Performance Tips

1. **Use render functions** for large datasets instead of pre-creating all elements
2. **Set appropriate overscan** values based on your scroll speed requirements
3. **Provide itemSize hints** when possible to reduce layout thrashing
4. **Use keepMounted sparingly** as it defeats the purpose of virtualization
5. **Optimize your item components** to avoid unnecessary re-renders

## Qwik-Specific Features

- **Resumability**: All state is serializable for optimal SSR/hydration
- **Fine-grained reactivity**: Uses Qwik signals for minimal re-renders
- **Lazy loading**: Components are loaded only when needed
- **QRL event handlers**: Proper Qwik event handling with `$` suffix

## Migration from React

The API is largely compatible with the React version:

```tsx
// React
import { Virtualizer } from "virtua";

// Qwik
import { Virtualizer } from "virtua/qwik";
```

Main differences:
- Event handlers use `$` suffix (`onScroll$` instead of `onScroll`)
- Refs use Qwik signals instead of React refs
- Components use `component$` instead of function components

## Examples

See the [example.tsx](./example.tsx) file for comprehensive usage examples including:
- Basic usage
- Render functions
- Imperative API
- Horizontal scrolling
- Variable item sizes
- Scroll event handling
- Custom components

## License

MIT
