/**
* Generic virtualized list component for ink-terminal.
*
* Wraps the useVirtualScroll hook into a declarative component API.
* Renders only items in viewport + overscan, using spacer Boxes to maintain
* scroll height. Works with any ScrollBox.
*
* Usage:
* ```tsx
*
* item.id}
* renderItem={(item, index) => }
* />
*
* ```
*/
import React, { type RefObject, useMemo } from 'react'
import type { ScrollBoxHandle } from '../components/ScrollBox.js'
import type { DOMElement } from '../dom.js'
import {
useVirtualScroll,
type VirtualScrollResult,
} from '../hooks/use-virtual-scroll.js'
import Box from './Box.js'
type VirtualListProps = {
/** Array of items to render. */
items: readonly T[]
/** Ref to the parent ScrollBox handle. */
scrollRef: RefObject
/** Terminal column count. Triggers height cache scaling on change. */
columns: number
/** Extract a unique key from each item. Must be stable across renders. */
itemKey: (item: T, index: number) => string
/** Render function for each visible item. */
renderItem: (item: T, index: number) => React.ReactNode
/**
* Optional ref for accessing the VirtualScrollResult imperatively.
* Useful for scrollToIndex, getItemTop, etc.
*/
resultRef?: React.MutableRefObject
}
/**
* A generic virtualized list that renders only visible items inside a ScrollBox.
*
* This component manages:
* - Mounting only items in viewport + overscan (80 rows above/below)
* - Height measurement and caching via Yoga layout
* - Top/bottom spacers to maintain scroll position
* - Slide cap (25 items/commit) for smooth fast scrolling
* - Column-change scaling for terminal resize
*/
export function VirtualList({
items,
scrollRef,
columns,
itemKey,
renderItem,
resultRef,
}: VirtualListProps): React.ReactNode {
// Build stable key array from items. useMemo so useVirtualScroll's
// itemKeys identity only changes when items actually change.
const itemKeys = useMemo(
() => items.map((item, i) => itemKey(item, i)),
[items, itemKey],
)
const result = useVirtualScroll(scrollRef, itemKeys, columns)
const { range, topSpacer, bottomSpacer, measureRef, spacerRef } = result
// Expose result for imperative access.
if (resultRef) {
resultRef.current = result
}
const [startIdx, endIdx] = range
// Render visible items with measurement refs.
const renderedItems: React.ReactNode[] = []
for (let i = startIdx; i < endIdx; i++) {
const item = items[i]!
const key = itemKeys[i]!
renderedItems.push(
}
flexDirection="column"
>
{renderItem(item, i)}
,
)
}
return (
<>
}
height={Math.max(0, Math.round(topSpacer))}
/>
{renderedItems}
>
)
}