[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: Akryum\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: 🐞 Bug report\ndescription: Report an issue with vue-virtual-scroller\nlabels: [to triage]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!\n      placeholder: Bug description\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Reproduction\n      description: Please provide a link to [StackBlitz](https://stackblitz.com/fork/vue). A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a \"need repro\" label. If no reproduction is provided after 3 days, it will be closed.\n      placeholder: Reproduction\n    validations:\n      required: true\n  - type: textarea\n    id: system-info\n    attributes:\n      label: System Info\n      description: Output of `npx envinfo --system --npmPackages '{vue,vue-virtual-scroller,vite,@vitejs/*}' --binaries --browsers`\n      render: shell\n      placeholder: System, Binaries, Browsers\n    validations:\n      required: true\n  - type: dropdown\n    id: package-manager\n    attributes:\n      label: Used Package Manager\n      description: Select the used package manager\n      options:\n        - npm\n        - yarn\n        - pnpm\n    validations:\n      required: true\n  - type: checkboxes\n    id: checkboxes\n    attributes:\n      label: Validations\n      description: Before submitting the issue, please make sure you do the following\n      options:\n        # - label: Follow our [Code of Conduct](https://github.com/histoire-dev/histoire/blob/main/CODE_OF_CONDUCT.md)\n        #   required: true\n        # - label: Read the [Contributing Guidelines](https://github.com/histoire-dev/histoire/blob/main/CONTRIBUTING.md).\n        #   required: true\n        - label: Read the [docs](https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md).\n          required: true\n        - label: Check that there isn't [already an issue](https://github.com/Akryum/vue-virtual-scroller/issues) that reports the same bug to avoid creating a duplicate.\n          required: true\n        - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/Akryum/vue-virtual-scroller/discussions).\n          required: true\n        - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Discussions\n    url: https://github.com/Akryum/vue-virtual-scroller/discussions\n    about: Use GitHub discussions for message-board style questions and discussions.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: 🚀 New feature proposal\ndescription: Propose a new feature to be added to vue-virtual-scroller\nlabels: ['enhancement: to triage']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in the project and taking the time to fill out this feature report!\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Clear and concise description of the problem\n      description: 'As a developer using vue-virtual-scroller I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!'\n    validations:\n      required: true\n  - type: textarea\n    id: suggested-solution\n    attributes:\n      label: Suggested solution\n      description: We could provide following implementation...\n    validations:\n      required: true\n  - type: textarea\n    id: alternative\n    attributes:\n      label: Alternative\n      description: Clear and concise description of any alternative solutions or features you've considered.\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional context\n      description: Any other context or screenshots about the feature request here.\n  - type: checkboxes\n    id: checkboxes\n    attributes:\n      label: Validations\n      description: Before submitting the issue, please make sure you do the following\n      options:\n        # - label: Follow our [Code of Conduct](https://github.com/histoire-dev/histoire/blob/main/CODE_OF_CONDUCT.md)\n        #   required: true\n        # - label: Read the [Contributing Guidelines](https://github.com/histoire-dev/histoire/blob/main/CONTRIBUTING.md).\n        #   required: true\n        - label: Read the [docs](https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md).\n          required: true\n        - label: Check that there isn't [already an issue](https://github.com/Akryum/vue-virtual-scroller/issues) that reports the same bug to avoid creating a duplicate.\n          required: true\n"
  },
  {
    "path": ".github/workflows/continuous-publish.yml",
    "content": "name: Publish Any Commit\non: [push, pull_request]\n\njobs:\n  auto-publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - run: corepack enable\n      - uses: actions/setup-node@v4\n        with:\n          node-version: latest\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      - run: pnpx pkg-pr-new publish './packages/*'\n"
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "content": "name: Check PR title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\njobs:\n  check-title:\n    runs-on: ubuntu-latest\n    steps:\n      # Please look up the latest version from\n      # https://github.com/amannn/action-semantic-pull-request/releases\n      - uses: amannn/action-semantic-pull-request@v3.4.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release-notes.yml",
    "content": "name: Create release\n\non:\n  push:\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  build:\n    name: Create Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@master\n        with:\n          fetch-depth: 0 # Fetch all tags\n\n      - name: Create Release for Tag\n        id: release_tag\n        uses: Akryum/release-tag@conventional\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          preset: angular # Use conventional-changelog preset\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - run: corepack enable\n      - uses: actions/setup-node@v4\n        with:\n          node-version: latest\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      - name: ESLint\n        run: pnpm lint\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n.temp/\n.cache/\ndist/\n.eslintcache\ndocs/.vitepress/cache\ndocs/.vitepress/dist\n"
  },
  {
    "path": ".node-version",
    "content": "25.8.0\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## v2.0.0-beta.10\n\n[compare changes](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.9...v2.0.0-beta.10)\n\n### 🩹 Fixes\n\n- Empty slot ([5791945](https://github.com/Akryum/vue-virtual-scroller/commit/5791945))\n\n### 🏡 Chore\n\n- Changelog ([2ae2195](https://github.com/Akryum/vue-virtual-scroller/commit/2ae2195))\n\n### ❤️ Contributors\n\n- Guillaume Chau ([@Akryum](http://github.com/Akryum))\n\n## v2.0.0-beta.9\n\n[compare changes](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.8...v2.0.0-beta.9)\n\n### 🚀 Enhancements\n\n- Items ref ([#789](https://github.com/Akryum/vue-virtual-scroller/pull/789))\n- New `disableTransform` prop to use top/left instead of translate ([#138](https://github.com/Akryum/vue-virtual-scroller/pull/138))\n- Typescript / composition rewrite, new docs ([dff69e8](https://github.com/Akryum/vue-virtual-scroller/commit/dff69e8))\n- AI skills ([8e58315](https://github.com/Akryum/vue-virtual-scroller/commit/8e58315))\n\n### 🩹 Fixes\n\n- Index lost, fix #783 ([#784](https://github.com/Akryum/vue-virtual-scroller/pull/784), [#783](https://github.com/Akryum/vue-virtual-scroller/issues/783))\n- Avoid rendering when slot is unused ([#787](https://github.com/Akryum/vue-virtual-scroller/pull/787))\n- Rewrite view (re-)assignment logic ([#743](https://github.com/Akryum/vue-virtual-scroller/pull/743))\n- **RecycleScroller:** Introduce an item wrapper to reduce re-render ([#742](https://github.com/Akryum/vue-virtual-scroller/pull/742))\n- Flicker issue in ios when scrolling up ([#864](https://github.com/Akryum/vue-virtual-scroller/pull/864))\n- Hide view to avoid overlap when position is set to -9999px; ([#837](https://github.com/Akryum/vue-virtual-scroller/pull/837))\n- Prevent empty gaps on fast scrolling, fix (#863, #882) ([#890](https://github.com/Akryum/vue-virtual-scroller/pull/890), [#863](https://github.com/Akryum/vue-virtual-scroller/issues/863), [#882](https://github.com/Akryum/vue-virtual-scroller/issues/882))\n- Recycle scroller visible gaps computation ([5940707](https://github.com/Akryum/vue-virtual-scroller/commit/5940707))\n- Small improvements ([013279c](https://github.com/Akryum/vue-virtual-scroller/commit/013279c))\n- Build ([8efdef2](https://github.com/Akryum/vue-virtual-scroller/commit/8efdef2))\n\n### 💅 Refactors\n\n- Esm only ([4ffe378](https://github.com/Akryum/vue-virtual-scroller/commit/4ffe378))\n\n### 📖 Documentation\n\n- Fix dead link ([c6a3320](https://github.com/Akryum/vue-virtual-scroller/commit/c6a3320))\n- Update readmes ([828c184](https://github.com/Akryum/vue-virtual-scroller/commit/828c184))\n\n### 🏡 Chore\n\n- Fix cherrypick of rewrite ([c9ccc34](https://github.com/Akryum/vue-virtual-scroller/commit/c9ccc34))\n- Update lockfile ([0f2e362](https://github.com/Akryum/vue-virtual-scroller/commit/0f2e362))\n- Update pnpm + pin pnpm in package.json ([#885](https://github.com/Akryum/vue-virtual-scroller/pull/885))\n- Add pkg.pr.new ([#886](https://github.com/Akryum/vue-virtual-scroller/pull/886))\n- Add test workflow ([#887](https://github.com/Akryum/vue-virtual-scroller/pull/887))\n- Update pnpm and refresh lockfile ([47efc94](https://github.com/Akryum/vue-virtual-scroller/commit/47efc94))\n\n### ✅ Tests\n\n- **lint:** Update eslint and use antfu config ([61b9919](https://github.com/Akryum/vue-virtual-scroller/commit/61b9919))\n- **lint:** Fix ([1e2e8e0](https://github.com/Akryum/vue-virtual-scroller/commit/1e2e8e0))\n\n### ❤️ Contributors\n\n- Guillaume Chau ([@Akryum](http://github.com/Akryum))\n- Ferflores507 ([@ferflores507](http://github.com/ferflores507))\n- KaygNas <597857074@QQ.COM>\n- Hobywhan ([@hobywhan](http://github.com/hobywhan))\n- Wan Zulsarhan Wan Shaari <zulsarhan.shaari@gmail.com>\n- Tatsuyuki Ishi ([@ishitatsuyuki](http://github.com/ishitatsuyuki))\n- AousAnwar ([@AousAnwar](http://github.com/AousAnwar))\n- Alex Liu ([@Mini-ghost](http://github.com/Mini-ghost))\n- Reynaldiaznan123 <reynaldiaznan450@gmail.com>\n- Vito ([@liu-lihao](http://github.com/liu-lihao))\n\n\n# [2.0.0-beta.8](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2023-02-06)\n\n\n\n### Bug Fixes\n\n* borderBoxSize not available in older browsers ([8f90971](https://github.com/Akryum/vue-virtual-scroller/commit/8f9097138d2f90ece8348141ac320c47ff7ab64a))\n\n\n\n# [2.0.0-beta.7](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2022-12-14)\n\n\n### Bug Fixes\n\n* items not updating if new object reference, fix [#690](https://github.com/Akryum/vue-virtual-scroller/issues/690) ([5b5df8c](https://github.com/Akryum/vue-virtual-scroller/commit/5b5df8cdc231f989e7fc6d6677d02e9ef695d1b9))\n\n\n\n# [2.0.0-beta.6](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2022-12-14)\n\n\n### Bug Fixes\n\n* keyField issue for class instances, fix [#770](https://github.com/Akryum/vue-virtual-scroller/issues/770) ([#771](https://github.com/Akryum/vue-virtual-scroller/issues/771)) ([1559ca8](https://github.com/Akryum/vue-virtual-scroller/commit/1559ca87e9195b6a1c5bada13de7f7b755a2fb6c))\n* **RecycleScroller:** gridItems is undefined when scrollToItem, fix [#773](https://github.com/Akryum/vue-virtual-scroller/issues/773) ([#761](https://github.com/Akryum/vue-virtual-scroller/issues/761)) ([7c809ad](https://github.com/Akryum/vue-virtual-scroller/commit/7c809ad1d612824867490c7bd5ce2861110412eb))\n* sorting views not working, [#772](https://github.com/Akryum/vue-virtual-scroller/issues/772) ([0b199d1](https://github.com/Akryum/vue-virtual-scroller/commit/0b199d14c846ecc00b93f989adbe29961dc68aad))\n* view not unused if item no longer present, fix [#774](https://github.com/Akryum/vue-virtual-scroller/issues/774) ([bd51403](https://github.com/Akryum/vue-virtual-scroller/commit/bd514031f537978f0343317bb9cee550c5bfd7ad))\n* views not reused correctly ([d5a8d75](https://github.com/Akryum/vue-virtual-scroller/commit/d5a8d759090f9af656865dd98648941fb2c71fa2))\n\n\n### Features\n\n* allow throttling update calls ([#764](https://github.com/Akryum/vue-virtual-scroller/issues/764)) ([9ba57d7](https://github.com/Akryum/vue-virtual-scroller/commit/9ba57d7d84c06d2ad265a266958292081704f218))\n\n\n\n# [2.0.0-beta.5](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2022-12-07)\n\n\n### Bug Fixes\n\n* duplicate active views ([1ef796b](https://github.com/Akryum/vue-virtual-scroller/commit/1ef796b42143da6d4e74f83b8ac88176128e6d77))\n* **DynamicScroller:** gaps caused by DOM reusing not triggering ResizeObserver ([a21e191](https://github.com/Akryum/vue-virtual-scroller/commit/a21e1915d76741a2806abd3a702d450f722879c8))\n* inconsistent state on reused view ([a14747d](https://github.com/Akryum/vue-virtual-scroller/commit/a14747d33d75eaf7fe820370436d70e82562939b))\n* views map corruption + view not removed from unusedPool ([cef8860](https://github.com/Akryum/vue-virtual-scroller/commit/cef886085c52f62736cf4c404a32f4f4fce6d229))\n\n\n### Performance Improvements\n\n* unnecessary loop ([86d0d07](https://github.com/Akryum/vue-virtual-scroller/commit/86d0d0776e26542d1b94484ec6ff5410733d3f18))\n\n\n\n# [2.0.0-beta.4](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2022-12-06)\n\n\n### Bug Fixes\n\n* improved dynamic scroller resize observer logic ([40f58b3](https://github.com/Akryum/vue-virtual-scroller/commit/40f58b3e3a411df36c09d59cc3776719f60d93cf))\n* item sizes getting 'disabled' resulting in gaps ([55b4ab1](https://github.com/Akryum/vue-virtual-scroller/commit/55b4ab1df1b4998178f2f03a53c112086a2633f2))\n* unusing views after non-continuous scroll ([11488b7](https://github.com/Akryum/vue-virtual-scroller/commit/11488b7d8ffdfe1384fe808e4a49c1ba95ad1383))\n* views incorrectly unused (proxy identity comparison) ([395bbfb](https://github.com/Akryum/vue-virtual-scroller/commit/395bbfb73588455795ecc5b144281ce5fda042ff))\n\n\n\n# [2.0.0-beta.3](https://github.com/Akryum/vue-virtual-scroller/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2022-10-18)\n\n\n### Performance Improvements\n\n* small code changes to maximize performance ([3b4dbf3](https://github.com/Akryum/vue-virtual-scroller/commit/3b4dbf39f480745d53e4bb43217c2b35975e4ab6))\n\n\n### Reverts\n\n* pass key-field prop [#732](https://github.com/Akryum/vue-virtual-scroller/issues/732), fix [#758](https://github.com/Akryum/vue-virtual-scroller/issues/758) ([8d221e6](https://github.com/Akryum/vue-virtual-scroller/commit/8d221e6978e4924ab125337fc91f6b6de7a1f497))\n\n\n\n# [2.0.0-beta.2](https://github.com/Akryum/vue-virtual-scroller/compare/v1.1.1...v2.0.0-beta.2) (2022-10-17)\n\n### Bug Fixes\n\n* fix: height NaN, fix [#757](https://github.com/Akryum/vue-virtual-scroller/issues/757)\n\n\n\n# [2.0.0-beta.1](https://github.com/Akryum/vue-virtual-scroller/compare/v1.1.0...v2.0.0-beta.1) (2022-10-15)\n\n\n### Bug Fixes\n\n* Account for the height of the leading and trailing slots when calculating visible items, fix [#685](https://github.com/Akryum/vue-virtual-scroller/issues/685) ([24ab3ba](https://github.com/Akryum/vue-virtual-scroller/commit/24ab3ba773d5819fcbe29f13eab663d48bce73ca))\n* avoid jumping scroll position when upper item size is calculated ([#374](https://github.com/Akryum/vue-virtual-scroller/issues/374)) ([fd58a95](https://github.com/Akryum/vue-virtual-scroller/commit/fd58a95392c98b8e67da66235fcf4cac78ea2fd4))\n* clamp endIndex if less items than prerender ([#473](https://github.com/Akryum/vue-virtual-scroller/issues/473)) ([f9124aa](https://github.com/Akryum/vue-virtual-scroller/commit/f9124aa81c36b46df339a5f18e0e832ab6e5a580))\n* DynamicScroller should pass its own keyField prop to child RecycleScroller ([#732](https://github.com/Akryum/vue-virtual-scroller/issues/732)) ([9673679](https://github.com/Akryum/vue-virtual-scroller/commit/9673679fc174cd6236fae4e19a9b1a3b625e900e))\n* **DynamicScrollerItem:** watch item prop ([#700](https://github.com/Akryum/vue-virtual-scroller/issues/700)) ([4d3b956](https://github.com/Akryum/vue-virtual-scroller/commit/4d3b95651610b8396c8dff66af9267407eab8e72))\n* issue with beforeDestroy hook ([#748](https://github.com/Akryum/vue-virtual-scroller/issues/748)) ([59f3f1b](https://github.com/Akryum/vue-virtual-scroller/commit/59f3f1b0aee9ab8ea276fee60e204b6dcc0baceb))\n* merge ([c8363b1](https://github.com/Akryum/vue-virtual-scroller/commit/c8363b114f691042dbced3b5b79d2ebd7812f481))\n* restore scroll in keep-alive ([#724](https://github.com/Akryum/vue-virtual-scroller/issues/724)) ([5011e06](https://github.com/Akryum/vue-virtual-scroller/commit/5011e06f2aa6ef8afa6ecaad804413e56a542c8d))\n* scrollToItem works with pageMode ([#396](https://github.com/Akryum/vue-virtual-scroller/issues/396)) ([c9772bf](https://github.com/Akryum/vue-virtual-scroller/commit/c9772bfb9e87672de1480072c4d5dc8024d1e5d1))\n* wrap the callback in requestAnimationFrame, fix [#516](https://github.com/Akryum/vue-virtual-scroller/issues/516) ([#517](https://github.com/Akryum/vue-virtual-scroller/issues/517)) ([6f359ab](https://github.com/Akryum/vue-virtual-scroller/commit/6f359abed6cf5d81a05d3760d6b622153f331f01))\n\n\n### Features\n\n* add an empty slot ([#398](https://github.com/Akryum/vue-virtual-scroller/issues/398)) ([5c2715c](https://github.com/Akryum/vue-virtual-scroller/commit/5c2715c0a2c52b0c27436baabbf982fcb9861131))\n* add skipHover prop to deactive the hover detection ([#752](https://github.com/Akryum/vue-virtual-scroller/issues/752)) ([b613318](https://github.com/Akryum/vue-virtual-scroller/commit/b613318a52d4d8f84bda69f0189f27dd51d0aaff))\n* adds configurable list/item tags for semantic html ([#203](https://github.com/Akryum/vue-virtual-scroller/issues/203)) ([3d24dc3](https://github.com/Akryum/vue-virtual-scroller/commit/3d24dc31928ec9eabe74294e5d5b3466109e1bc2))\n* custom classes for list wrapper and list items. ([#397](https://github.com/Akryum/vue-virtual-scroller/issues/397)) ([32b285d](https://github.com/Akryum/vue-virtual-scroller/commit/32b285d40667870b65c71dc59b02627f97c67ea4))\n* Emit events for scroll to begin and end of list ([#364](https://github.com/Akryum/vue-virtual-scroller/issues/364)) ([2a7bfd4](https://github.com/Akryum/vue-virtual-scroller/commit/2a7bfd45e1ee56e82426a67d9f3f3ba5a7839185))\n* gridItems prop ([#27](https://github.com/Akryum/vue-virtual-scroller/issues/27)) ([6339e72](https://github.com/Akryum/vue-virtual-scroller/commit/6339e72693c982805648ae3001b7c2957d8aa39e))\n* itemSecondarySize ([43d311c](https://github.com/Akryum/vue-virtual-scroller/commit/43d311c2f336de74da4d0ec705b0a3546eeda153))\n* throw error when key field does not exist in item ([#265](https://github.com/Akryum/vue-virtual-scroller/issues/265)) ([c63129f](https://github.com/Akryum/vue-virtual-scroller/commit/c63129fdc8264d25c737db1c2ce2891a9b804705))\n* update event provide range of the visible items ([#115](https://github.com/Akryum/vue-virtual-scroller/issues/115)) ([f19af6c](https://github.com/Akryum/vue-virtual-scroller/commit/f19af6c15346ff33e5d3c4b9729b02a73d5fe4df))\n\n\n### Performance Improvements\n\n* skipHover: don't add event listeners ([6b623b5](https://github.com/Akryum/vue-virtual-scroller/commit/6b623b56e4ab481b1e0cde883682df2cc81edf19))\n\n\n\n"
  },
  {
    "path": "README.md",
    "content": "# vue-virtual-scroller\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://www.npmjs.com/package/vue-virtual-scroller)\n[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)\n\n[Documentation](https://vue-virtual-scroller.netlify.app/)\n\nBlazing fast scrolling of any amount of data | [Live demo](https://vue-virtual-scroller-demo.netlify.app/) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)\n\nFor Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller)\n\nThis package ships ESM only in the current Vue 3 line. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.\n\n[💚️ Become a Sponsor](https://github.com/sponsors/Akryum)\n\n## Sponsors\n\n<p align=\"center\">\n  <a href=\"https://guillaume-chau.info/sponsors/\" target=\"_blank\">\n    <img src='https://akryum.netlify.app/sponsors.svg' alt=\"sponsors\" />\n  </a>\n</p>\n"
  },
  {
    "path": "SKILLS-GENERATION.md",
    "content": "# Skills Generation (vue-virtual-scroller)\n\nThis file is the canonical process for generating and updating package skills in this repository.\n\n## Scope\n\nThis process currently covers one package skill:\n\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller`\n\nThis process does not cover:\n\n- `packages/demo` as a standalone skill target\n- internal implementation-only helpers that are not documented as public APIs\n\n## Sources of truth\n\nAlways generate skill content from public documentation first, not from memory.\n\nIf implementation behavior appears to differ from docs, fix docs first, then regenerate the skill from the updated docs.\n\n### Primary docs\n\n- `docs/index.md`\n- `docs/guide/index.md`\n- `docs/guide/recycle-scroller.md`\n- `docs/guide/dynamic-scroller.md`\n- `docs/guide/dynamic-scroller-item.md`\n- `docs/guide/id-state.md`\n- `docs/guide/use-recycle-scroller.md`\n\n### Supporting examples\n\nUse these to sharpen examples and workflow guidance, not to invent undocumented API behavior:\n\n- `docs/demos/index.md`\n- `docs/demos/recycle-scroller.md`\n- `docs/demos/dynamic-scroller.md`\n- `docs/demos/chat.md`\n- `docs/demos/simple-list.md`\n- `docs/demos/horizontal.md`\n- `docs/demos/grid.md`\n- `docs/demos/test-chat.md`\n- `packages/demo/src/**`\n\n### Public-export verification\n\nUse these only to verify package exports, option names, and known docs gaps:\n\n- `packages/vue-virtual-scroller/src/index.ts`\n- `packages/vue-virtual-scroller/src/types.ts`\n- `packages/vue-virtual-scroller/README.md`\n\nIf an exported surface is not documented enough to support skill content, update docs first or explicitly leave that surface out of the generated skill.\n\nCurrent areas that require extra care:\n\n- `docs/guide/id-state.md` describes `IdState`, while the current package exports `useIdState`\n- `useDynamicScroller` and `useDynamicScrollerItem` are exported but do not currently have guide pages\n- plugin install options such as `installComponents` and `componentsPrefix` are exported but not fully documented in the guide\n\nDo not silently fill those gaps from source code into the skill. Either document them first or keep them out of the generated skill.\n\n## Output files\n\nGenerate one skill folder for the published package:\n\n1. `packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md`\n2. `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md`\n3. One reference file per documented public surface or recurring workflow\n\nExpected initial reference set for this repo:\n\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md`\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md`\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md`\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md`\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md`\n\nOptional reference files:\n\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/id-state.md` only after docs and exports are reconciled\n- `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/patterns-and-guardrails.md` if the core skill becomes too dense\n\nDo not force an `api-*.md` naming pattern here. This repo is better represented by one file per component, composable, or usage decision.\n\n## Required `SKILL.md` structure\n\nEach generated `SKILL.md` should include:\n\n1. YAML frontmatter:\n   - `name`\n   - `description` as a single line that clearly mentions Vue virtual scrolling, `RecycleScroller`, `DynamicScroller`, and headless usage so the skill triggers correctly\n2. Title and one-line summary\n3. A quick decision table for when to use:\n   - `RecycleScroller`\n   - `DynamicScroller`\n   - `DynamicScrollerItem`\n   - `useRecycleScroller`\n4. Setup snippet that includes:\n   - package install\n   - ESM-only note\n   - CSS import (`vue-virtual-scroller/index.css`)\n   - plugin install or direct component import\n5. Practical guidance sections for:\n   - choosing fixed-size vs variable-size rendering\n   - when to switch from `RecycleScroller` to `DynamicScroller`\n   - required sizing/CSS constraints\n   - performance guardrails and reuse pitfalls\n   - common layouts such as chat feeds, grids, and horizontal scrollers\n6. References section containing a table with `Topic`, `Description`, and `Reference`\n7. Further reading section linking only to shipped reference files and, if needed, stable package-level external URLs\n\n## Required references structure\n\nEach skill should include a `references/` folder with surface-focused reference files. Keep references one level deep from `SKILL.md`.\n\nMandatory layout:\n\n- `references/index.md`: maps each documented surface and workflow topic to exactly one reference file\n- `references/<surface>.md`: one file per documented public surface or focused workflow\n\nEach reference file should:\n\n- start with a short title and one-line scope\n- include a short provenance section without referencing repository-local file paths outside the published package\n- include sections in this order when possible:\n  - `When to use`\n  - `Required inputs`\n  - `Core props/options`\n  - `Events/returns`\n  - `Pitfalls`\n  - `Example patterns`\n- stay grounded in current docs\n- focus on user-facing behavior, not internal implementation detail\n- avoid chaining into nested references\n- avoid bundling unrelated surfaces into one file\n\n## Writing constraints\n\n- Keep guidance practical and tied to current public behavior.\n- Prefer decisions and guardrails over generic marketing language.\n- Always mention that the package is Vue 3 and ESM-only when setup is discussed.\n- Always mention the required CSS import when installation/setup is discussed.\n- Do not invent APIs, props, events, or helper functions that are not documented.\n- Do not describe Vue 2 usage in the generated skill for this repo.\n- Use demo pages to illustrate patterns such as chat streams, grids, horizontal scrolling, and stress-tested append flows.\n- Never reference repository-local files outside the published package from `SKILL.md` or `references/*.md`.\n- In shipped skill files, only link to other shipped skill files or stable external package URLs.\n- If the repo documents AI-agent consumption with `skills-npm`, keep that guidance in the VitePress docs, not in the shipped skill files.\n- Do not generate or update `agents/openai.yaml` for this workflow.\n\n## Generation workflow\n\n### 1. Gather context\n\n```bash\nrg --files docs packages/vue-virtual-scroller\nrg -n \"RecycleScroller|DynamicScroller|DynamicScrollerItem|useRecycleScroller|useDynamicScroller|useDynamicScrollerItem|useIdState|installComponents|componentsPrefix|ESM\" \\\n  docs \\\n  packages/vue-virtual-scroller/src/index.ts \\\n  packages/vue-virtual-scroller/src/types.ts \\\n  packages/vue-virtual-scroller/README.md\n```\n\nRead the primary docs files first.\n\nUse demos and package exports to check scope and examples.\n\nIf docs are missing, outdated, or contradictory, update docs first and use the updated docs as the generation input.\n\n### 2. Decide skill coverage\n\nStart from documented public surfaces only.\n\nFor the current repo baseline, the minimum covered surfaces should be:\n\n- installation/setup\n- `RecycleScroller`\n- `DynamicScroller`\n- `DynamicScrollerItem`\n- `useRecycleScroller`\n\nOnly add `id-state`, `useDynamicScroller`, `useDynamicScrollerItem`, or plugin option references after the docs clearly support them.\n\n### 3. Generate or update the skill\n\n- Regenerate `SKILL.md` using the required structure above.\n- Regenerate `references/index.md`.\n- Regenerate one reference file per documented surface or workflow topic.\n- Ensure `SKILL.md` links to `references/index.md`.\n- Keep the top-level skill concise and move detail into `references/*.md`.\n\n### 4. Validate generated skill files\n\n```bash\nsed -n '1,260p' packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md\nsed -n '1,260p' packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md\nrg --files packages/vue-virtual-scroller/skills/vue-virtual-scroller/references\n```\n\nChecklist:\n\n- [ ] Frontmatter is valid and the description is specific enough to trigger on virtual scrolling tasks.\n- [ ] Setup guidance includes the ESM-only note and the CSS import.\n- [ ] The choose-the-right-surface guidance distinguishes `RecycleScroller`, `DynamicScroller`, and headless usage correctly.\n- [ ] Fixed-size, variable-size, page mode, and common performance pitfalls are grounded in current docs.\n- [ ] `references/index.md` exists and links to every reference file.\n- [ ] Each reference file covers one surface or one focused workflow only.\n- [ ] No shipped skill file links to repository-local paths outside the package.\n- [ ] Any API not fully documented in the guide has been omitted or documented first.\n\n### 5. Record generation metadata\n\nAfter regeneration, update this document with:\n\n- generation date\n- docs/package baseline commit SHA\n- version notes if the public package surface changed\n- generated artifacts\n\n## Incremental update process\n\nWhen docs or public exports change, update only impacted skill sections.\n\n```bash\ngit diff <last_skill_sha>..HEAD -- docs packages/vue-virtual-scroller/src/index.ts packages/vue-virtual-scroller/src/types.ts packages/vue-virtual-scroller/README.md\ngit diff --name-only <last_skill_sha>..HEAD -- docs packages/vue-virtual-scroller/src/index.ts packages/vue-virtual-scroller/src/types.ts packages/vue-virtual-scroller/README.md\n```\n\nThen:\n\n1. Map changed docs or export files to affected skill sections.\n2. If exports changed without docs updates, fix docs first unless the skill already omits that surface.\n3. Update only the affected `SKILL.md` sections and reference files.\n4. Re-run the validation checklist.\n5. Refresh metadata below.\n\n## Current generation metadata\n\n- Last generation date: `2026-03-10T14:40:32+01:00`\n- Baseline commit SHA: `4ffe378192353c24e474d7541c649613458cf1eb`\n- Baseline short SHA: `4ffe378`\n- Baseline commit date: `2026-03-10T14:25:42+01:00`\n- Baseline commit message: `refactor: esm only`\n- Generated artifacts:\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md`\n  - `packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md`\n\n## Notes\n\n- There is no dedicated generation script in this repository yet.\n- Generation is currently a documented manual process with reproducible inspection commands.\n- `packages/demo` exists as an example application and validation aid, not as a primary skill output target.\n- To make shipped skills consumable through `skills-npm`, the published `vue-virtual-scroller` package must include the `skills/` directory in its packaged files.\n"
  },
  {
    "path": "docs/.vitepress/components/demos/ChatStreamDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'\nimport DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'\nimport { avatarStyle, createMessages } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst scroller = ref<InstanceType<typeof DynamicScroller>>()\nconst basePool = createMessages(1500, 303)\n\nlet nextId = 1\nconst stream = ref(createMessages(20, 707).map(item => ({ ...item, id: nextId++ })))\nconst search = ref('')\nconst streaming = ref(false)\n\nlet streamTimer: ReturnType<typeof setInterval> | undefined\n\nconst filteredItems = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return stream.value\n  return stream.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))\n})\n\nfunction appendBatch(amount = 8) {\n  for (let i = 0; i < amount; i++) {\n    const template = basePool[(nextId + i) % basePool.length]\n    stream.value.push({\n      ...template,\n      id: nextId++,\n      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),\n    })\n  }\n  requestAnimationFrame(() => scroller.value?.scrollToBottom())\n}\n\nfunction startStream() {\n  if (streaming.value)\n    return\n  streaming.value = true\n  appendBatch(12)\n  streamTimer = setInterval(() => {\n    appendBatch(6)\n  }, 320)\n}\n\nfunction stopStream() {\n  streaming.value = false\n  if (streamTimer) {\n    clearInterval(streamTimer)\n    streamTimer = undefined\n  }\n}\n\nonBeforeUnmount(stopStream)\n</script>\n\n<template>\n  <DemoShell\n    title=\"Chat stream\"\n    description=\"Ported from the streaming chat demo. New rows are pushed continuously and the view auto-scrolls to bottom.\"\n  >\n    <template #toolbar>\n      <button\n        v-if=\"!streaming\"\n        class=\"demo-button\"\n        @click=\"startStream\"\n      >\n        Start stream\n      </button>\n      <button\n        v-else\n        class=\"demo-button secondary\"\n        @click=\"stopStream\"\n      >\n        Stop stream\n      </button>\n\n      <button\n        class=\"demo-button secondary\"\n        @click=\"appendBatch(20)\"\n      >\n        +20 messages\n      </button>\n\n      <label class=\"demo-chip\">\n        Filter\n        <input\n          v-model=\"search\"\n          type=\"text\"\n          placeholder=\"Search\"\n        >\n      </label>\n\n      <span class=\"demo-chip\">Rows: {{ filteredItems.length }}</span>\n    </template>\n\n    <DynamicScroller\n      ref=\"scroller\"\n      class=\"demo-viewport\"\n      :items=\"filteredItems\"\n      :min-item-size=\"62\"\n    >\n      <template #before>\n        <div class=\"demo-notice\">\n          The Stream demo appends data in real time while preserving smooth scrolling.\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[item.message]\"\n          class=\"demo-message-row\"\n        >\n          <div\n            class=\"demo-avatar\"\n            :style=\"avatarStyle(item.hue)\"\n          >\n            {{ item.initials }}\n          </div>\n\n          <div class=\"demo-chat-bubble\">\n            <strong>{{ item.user }}</strong>\n            <div class=\"demo-message-body\">\n              {{ item.message }}\n            </div>\n          </div>\n\n          <small class=\"demo-message-meta\">#{{ index }} · {{ item.timestamp }}</small>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/DemoShell.vue",
    "content": "<script setup lang=\"ts\">\ndefineProps<{\n  title: string\n  description: string\n}>()\n</script>\n\n<template>\n  <section class=\"demo-shell\">\n    <header class=\"demo-shell__header\">\n      <h3 class=\"demo-shell__title\">\n        {{ title }}\n      </h3>\n      <p class=\"demo-shell__description\">\n        {{ description }}\n      </p>\n    </header>\n    <div class=\"demo-shell__toolbar\">\n      <slot name=\"toolbar\" />\n    </div>\n    <div class=\"demo-shell__viewport\">\n      <slot />\n    </div>\n  </section>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/DynamicScrollerDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'\nimport DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'\nimport { avatarStyle, createMessages, mutateMessage } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst search = ref('')\nconst messages = ref(createMessages(600, 101))\nconst minItemSize = ref(68)\n\nconst visibleStart = ref(0)\nconst visibleEnd = ref(0)\n\nconst filteredMessages = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return messages.value\n  return messages.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))\n})\n\nfunction randomizeMessage(index: number) {\n  const row = filteredMessages.value[index]\n  if (!row)\n    return\n  mutateMessage(row, Date.now() % 997)\n}\n\nfunction onUpdate(_viewStart: number, _viewEnd: number, start: number, end: number) {\n  visibleStart.value = start\n  visibleEnd.value = end\n}\n</script>\n\n<template>\n  <DemoShell\n    title=\"DynamicScroller: unknown heights\"\n    description=\"Ported from the dynamic messages demo. Each row recalculates as content changes.\"\n  >\n    <template #toolbar>\n      <label class=\"demo-chip\">\n        Filter\n        <input\n          v-model=\"search\"\n          type=\"text\"\n          placeholder=\"Type keyword\"\n        >\n      </label>\n\n      <label class=\"demo-chip\">\n        Min row size\n        <input\n          v-model.number=\"minItemSize\"\n          type=\"range\"\n          min=\"40\"\n          max=\"120\"\n          step=\"2\"\n        >\n        {{ minItemSize }}px\n      </label>\n\n      <span class=\"demo-chip\">Matches: {{ filteredMessages.length }}</span>\n      <span class=\"demo-chip\">Visible: {{ visibleStart }}-{{ visibleEnd }}</span>\n    </template>\n\n    <DynamicScroller\n      class=\"demo-viewport\"\n      :items=\"filteredMessages\"\n      :min-item-size=\"minItemSize\"\n      :emit-update=\"true\"\n      @update=\"onUpdate\"\n    >\n      <template #before>\n        <div class=\"demo-notice\">\n          Click any message to mutate text and trigger a dynamic size recalculation.\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[item.message]\"\n          class=\"demo-message-row\"\n          @click=\"randomizeMessage(index)\"\n        >\n          <div\n            class=\"demo-avatar\"\n            :style=\"avatarStyle(item.hue)\"\n          >\n            {{ item.initials }}\n          </div>\n\n          <div>\n            <div class=\"demo-message-body\">\n              {{ item.message }}\n            </div>\n            <small class=\"demo-message-meta\">{{ item.user }}</small>\n          </div>\n\n          <small class=\"demo-message-meta\">{{ item.timestamp }}</small>\n        </DynamicScrollerItem>\n      </template>\n\n      <template #after>\n        <div class=\"demo-notice\">\n          End of list.\n        </div>\n      </template>\n    </DynamicScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/GridDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Person } from './demo-data'\nimport { computed, ref } from 'vue'\nimport RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'\nimport { createPeopleRows, gradientAt } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\ninterface GridCard extends Person {\n  id: number\n}\n\nconst scroller = ref<InstanceType<typeof RecycleScroller>>()\nconst gridItems = ref(5)\nconst scrollTo = ref(300)\n\nconst rawRows = createPeopleRows(2500, false, 111)\n\nconst cards = computed<GridCard[]>(() =>\n  rawRows\n    .filter(row => row.type === 'person')\n    .map((row) => {\n      const person = row.value as Person\n      return {\n        id: row.id,\n        ...person,\n      }\n    }),\n)\n\nfunction jump() {\n  const target = Math.min(Math.max(0, scrollTo.value), cards.value.length - 1)\n  scroller.value?.scrollToItem(target)\n}\n</script>\n\n<template>\n  <DemoShell\n    title=\"Grid mode\"\n    description=\"Ported from the grid demo. RecycleScroller composes fixed-size cards in rows for large image-like layouts.\"\n  >\n    <template #toolbar>\n      <label class=\"demo-chip\">\n        Items / row\n        <input\n          v-model.number=\"gridItems\"\n          type=\"range\"\n          min=\"2\"\n          max=\"10\"\n        >\n        {{ gridItems }}\n      </label>\n\n      <label class=\"demo-chip\">\n        Scroll to\n        <input\n          v-model.number=\"scrollTo\"\n          type=\"number\"\n          min=\"0\"\n          :max=\"cards.length\"\n        >\n      </label>\n\n      <button\n        class=\"demo-button\"\n        @click=\"jump\"\n      >\n        Jump\n      </button>\n\n      <span class=\"demo-chip\">Cards: {{ cards.length }}</span>\n    </template>\n\n    <RecycleScroller\n      ref=\"scroller\"\n      class=\"demo-viewport\"\n      :items=\"cards\"\n      :item-size=\"166\"\n      :grid-items=\"gridItems\"\n      :item-secondary-size=\"176\"\n    >\n      <template #default=\"{ item, index }\">\n        <article class=\"demo-grid-card\" :style=\"{ background: gradientAt(index) }\">\n          <small>#{{ index }}</small>\n          <strong>{{ item.initials }}</strong>\n          <span>{{ item.name }}</span>\n        </article>\n      </template>\n    </RecycleScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/HorizontalDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'\nimport DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'\nimport { avatarStyle, createMessages } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst search = ref('')\nconst rows = ref(createMessages(500, 909))\n\nconst filteredRows = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return rows.value\n  return rows.value.filter(row => row.message.toLowerCase().includes(term) || row.user.toLowerCase().includes(term))\n})\n\nfunction cardWidth(message: string) {\n  return Math.max(180, Math.min(440, Math.round(message.length * 0.95)))\n}\n</script>\n\n<template>\n  <DemoShell\n    title=\"Horizontal dynamic\"\n    description=\"Ported from the horizontal demo. Unknown widths are measured dynamically while scrolling on the x-axis.\"\n  >\n    <template #toolbar>\n      <label class=\"demo-chip\">\n        Filter\n        <input\n          v-model=\"search\"\n          type=\"text\"\n          placeholder=\"Search text\"\n        >\n      </label>\n\n      <span class=\"demo-chip\">Cards: {{ filteredRows.length }}</span>\n      <span class=\"demo-chip\">Tip: Shift + wheel for horizontal scroll</span>\n    </template>\n\n    <DynamicScroller\n      class=\"demo-viewport demo-horizontal-track\"\n      :items=\"filteredRows\"\n      :min-item-size=\"180\"\n      direction=\"horizontal\"\n    >\n      <template #before>\n        <div class=\"demo-notice\">\n          Width is content-driven and recalculated per card.\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[item.message]\"\n          :style=\"{ width: `${cardWidth(item.message)}px` }\"\n          class=\"demo-horizontal-card\"\n        >\n          <div class=\"demo-avatar\" :style=\"avatarStyle(item.hue)\">\n            {{ item.initials }}\n          </div>\n          <div class=\"demo-message-body\">\n            {{ item.message }}\n          </div>\n          <small class=\"demo-message-meta\">{{ item.user }} · #{{ index }}</small>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/RecycleScrollerDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Person, PersonRow } from './demo-data'\nimport { computed, onMounted, ref, watch } from 'vue'\nimport RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'\nimport { avatarStyle, createPeopleRows } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst scroller = ref<InstanceType<typeof RecycleScroller>>()\nconst count = ref(8000)\nconst withLetters = ref(true)\nconst buffer = ref(240)\nconst scrollTo = ref(180)\nconst rows = ref<PersonRow[]>([])\n\nconst visibleStart = ref(0)\nconst visibleEnd = ref(0)\n\nconst itemSize = computed(() => (withLetters.value ? null : 74))\n\nfunction regenerate() {\n  rows.value = createPeopleRows(Math.max(50, count.value), withLetters.value, 17)\n}\n\nfunction addPeople(amount = 100) {\n  count.value = Math.min(50000, count.value + amount)\n}\n\nfunction jump() {\n  const target = Math.min(Math.max(0, scrollTo.value), rows.value.length - 1)\n  scroller.value?.scrollToItem(target)\n}\n\nfunction toggleLetterSize(row: PersonRow) {\n  if (row.type === 'letter') {\n    row.height = row.height === 96 ? 136 : 96\n  }\n}\n\nfunction personOf(row: PersonRow) {\n  return row.value as Person\n}\n\nfunction onUpdate(_viewStart: number, _viewEnd: number, start: number, end: number) {\n  visibleStart.value = start\n  visibleEnd.value = end\n}\n\nwatch([count, withLetters], regenerate)\nonMounted(regenerate)\n</script>\n\n<template>\n  <DemoShell\n    title=\"RecycleScroller: Large list, variable height\"\n    description=\"Ported from the classic RecycleScroller demo with modern controls and cleaner visual feedback.\"\n  >\n    <template #toolbar>\n      <label class=\"demo-chip\">\n        Items\n        <input\n          v-model.number=\"count\"\n          type=\"number\"\n          min=\"50\"\n          max=\"50000\"\n        >\n      </label>\n\n      <label class=\"demo-chip\">\n        Variable height\n        <input\n          v-model=\"withLetters\"\n          type=\"checkbox\"\n        >\n      </label>\n\n      <label class=\"demo-chip\">\n        Buffer\n        <input\n          v-model.number=\"buffer\"\n          type=\"range\"\n          min=\"100\"\n          max=\"1800\"\n          step=\"20\"\n        >\n        {{ buffer }}px\n      </label>\n\n      <label class=\"demo-chip\">\n        Scroll to\n        <input\n          v-model.number=\"scrollTo\"\n          type=\"number\"\n          min=\"0\"\n          :max=\"rows.length\"\n        >\n      </label>\n\n      <button\n        class=\"demo-button secondary\"\n        @click=\"addPeople(500)\"\n      >\n        +500\n      </button>\n\n      <button\n        class=\"demo-button\"\n        @click=\"jump\"\n      >\n        Jump\n      </button>\n\n      <span class=\"demo-chip\">Visible: {{ visibleStart }}-{{ visibleEnd }}</span>\n    </template>\n\n    <RecycleScroller\n      ref=\"scroller\"\n      class=\"demo-viewport\"\n      :items=\"rows\"\n      :item-size=\"itemSize\"\n      :buffer=\"buffer\"\n      key-field=\"id\"\n      size-field=\"height\"\n      :emit-update=\"true\"\n      @update=\"onUpdate\"\n    >\n      <template #default=\"{ item, index }\">\n        <div\n          v-if=\"item.type === 'letter'\"\n          class=\"demo-letter-row\"\n          :style=\"{ height: `${item.height}px` }\"\n          @click=\"toggleLetterSize(item)\"\n        >\n          <strong>{{ item.value }}</strong>\n          <span>Segment {{ index }}</span>\n        </div>\n\n        <div\n          v-else\n          class=\"demo-person-row\"\n          :style=\"{ height: `${item.height}px` }\"\n        >\n          <div\n            class=\"demo-avatar\"\n            :style=\"avatarStyle(personOf(item).hue)\"\n          >\n            {{ personOf(item).initials }}\n          </div>\n\n          <div>\n            <div>{{ personOf(item).name }}</div>\n            <small class=\"demo-message-meta\">Click letter rows to toggle heights</small>\n          </div>\n\n          <small class=\"demo-message-meta\">#{{ index }}</small>\n        </div>\n      </template>\n    </RecycleScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/SimpleListDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'\nimport DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'\nimport RecycleScroller from '../../../../packages/vue-virtual-scroller/src/components/RecycleScroller.vue'\nimport { createSimpleStrings } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst useDynamic = ref(true)\nconst search = ref('')\nconst rows = ref(createSimpleStrings(4000, 505))\n\nconst filteredRows = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return rows.value\n  return rows.value.filter(item => item.toLowerCase().includes(term))\n})\n</script>\n\n<template>\n  <DemoShell\n    title=\"Simple list\"\n    description=\"Ported from the simple-list demo. Switch between DynamicScroller and RecycleScroller with a single control.\"\n  >\n    <template #toolbar>\n      <label class=\"demo-chip\">\n        Filter\n        <input\n          v-model=\"search\"\n          type=\"text\"\n          placeholder=\"Find sentence\"\n        >\n      </label>\n\n      <label class=\"demo-chip\">\n        Dynamic mode\n        <input\n          v-model=\"useDynamic\"\n          type=\"checkbox\"\n        >\n      </label>\n\n      <span class=\"demo-chip\">Rows: {{ filteredRows.length }}</span>\n    </template>\n\n    <DynamicScroller\n      v-if=\"useDynamic\"\n      class=\"demo-viewport\"\n      :items=\"filteredRows\"\n      :min-item-size=\"58\"\n    >\n      <template #before>\n        <div class=\"demo-notice\">\n          Dynamic mode handles variable sentence height.\n        </div>\n      </template>\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :index=\"index\"\n          :size-dependencies=\"[item]\"\n          class=\"demo-message-row\"\n        >\n          <div class=\"demo-avatar\" :style=\"{ background: 'linear-gradient(145deg, #4a7c59, #234f35)' }\">\n            {{ String(index + 1).slice(-2).padStart(2, '0') }}\n          </div>\n          <div class=\"demo-message-body\">\n            {{ item }}\n          </div>\n          <small class=\"demo-message-meta\">dynamic</small>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n\n    <RecycleScroller\n      v-else\n      class=\"demo-viewport\"\n      :items=\"filteredRows\"\n      :item-size=\"58\"\n    >\n      <template #default=\"{ item, index }\">\n        <div class=\"demo-message-row\">\n          <div class=\"demo-avatar\" :style=\"{ background: 'linear-gradient(145deg, #7b2cbf, #3c096c)' }\">\n            {{ String(index + 1).slice(-2).padStart(2, '0') }}\n          </div>\n          <div class=\"demo-message-body\">\n            {{ item }}\n          </div>\n          <small class=\"demo-message-meta\">fixed</small>\n        </div>\n      </template>\n    </RecycleScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/TestChatDocDemo.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport DynamicScroller from '../../../../packages/vue-virtual-scroller/src/components/DynamicScroller.vue'\nimport DynamicScrollerItem from '../../../../packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue'\nimport { createSimpleStrings } from './demo-data'\nimport DemoShell from './DemoShell.vue'\n\nconst pool = createSimpleStrings(1200, 1303)\nconst scroller = ref<InstanceType<typeof DynamicScroller>>()\nconst rows = ref<{ id: number, text: string }[]>([])\n\nlet nextId = 1\n\nfunction addItems(count = 1) {\n  for (let i = 0; i < count; i++) {\n    rows.value.push({\n      id: nextId,\n      text: pool[nextId % pool.length],\n    })\n    nextId++\n  }\n  requestAnimationFrame(() => scroller.value?.scrollToBottom())\n}\n</script>\n\n<template>\n  <DemoShell\n    title=\"Test chat append\"\n    description=\"Ported from test-chat. This stress test appends many rows and keeps the viewport pinned to the latest messages.\"\n  >\n    <template #toolbar>\n      <button class=\"demo-button\" @click=\"addItems(1)\">\n        +1\n      </button>\n      <button class=\"demo-button\" @click=\"addItems(5)\">\n        +5\n      </button>\n      <button class=\"demo-button\" @click=\"addItems(20)\">\n        +20\n      </button>\n      <button class=\"demo-button\" @click=\"addItems(80)\">\n        +80\n      </button>\n      <span class=\"demo-chip\">Messages: {{ rows.length }}</span>\n    </template>\n\n    <DynamicScroller\n      ref=\"scroller\"\n      class=\"demo-viewport\"\n      :items=\"rows\"\n      :min-item-size=\"48\"\n      @resize=\"scroller?.scrollToBottom()\"\n    >\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[item.text]\"\n          class=\"demo-message-row\"\n        >\n          <div class=\"demo-avatar\" :style=\"{ background: 'linear-gradient(145deg, #2f7a52, #14532d)' }\">\n            {{ String((index % 99) + 1).padStart(2, '0') }}\n          </div>\n          <div class=\"demo-chat-bubble\">\n            <div class=\"demo-message-body\">\n              {{ item.text }}\n            </div>\n          </div>\n          <small class=\"demo-message-meta\">#{{ item.id }}</small>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </DemoShell>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/demos/demo-data.ts",
    "content": "export interface PersonRow {\n  id: number\n  index: number\n  type: 'person' | 'letter'\n  value: string | Person\n  height: number\n}\n\nexport interface Person {\n  name: string\n  initials: string\n  hue: number\n}\n\nexport interface MessageRow {\n  id: number\n  user: string\n  initials: string\n  hue: number\n  message: string\n  timestamp: string\n}\n\nconst FIRST_NAMES = [\n  'Avery',\n  'Riley',\n  'Jordan',\n  'Quinn',\n  'Morgan',\n  'Rowan',\n  'Sage',\n  'Parker',\n  'Casey',\n  'Reese',\n  'Dakota',\n  'Alex',\n  'Jamie',\n  'Taylor',\n  'Harper',\n  'Mika',\n  'Noa',\n  'Arden',\n  'River',\n  'Kai',\n]\n\nconst LAST_NAMES = [\n  'Anderson',\n  'Bennett',\n  'Carter',\n  'Diaz',\n  'Edwards',\n  'Fletcher',\n  'Garcia',\n  'Hughes',\n  'Ingram',\n  'Johnson',\n  'Khan',\n  'Lopez',\n  'Miller',\n  'Nguyen',\n  'Ortiz',\n  'Patel',\n  'Quincy',\n  'Rivera',\n  'Sato',\n  'Turner',\n]\n\nconst WORDS = [\n  'virtual',\n  'scrolling',\n  'profile',\n  'buffer',\n  'dynamic',\n  'render',\n  'smooth',\n  'window',\n  'active',\n  'message',\n  'compute',\n  'layout',\n  'viewport',\n  'recycle',\n  'velocity',\n  'index',\n  'height',\n  'width',\n  'visibility',\n  'performant',\n  'batch',\n  'stream',\n  'queue',\n  'resize',\n  'observe',\n  'compose',\n  'discover',\n  'cluster',\n  'card',\n  'slot',\n]\n\nfunction createRng(seed = 1) {\n  let value = seed >>> 0\n  return () => {\n    value = (Math.imul(1664525, value) + 1013904223) >>> 0\n    return value / 4294967296\n  }\n}\n\nfunction pick<T>(rng: () => number, values: T[]) {\n  return values[Math.floor(rng() * values.length)]\n}\n\nfunction capitalize(text: string) {\n  return text.charAt(0).toUpperCase() + text.slice(1)\n}\n\nfunction sentence(rng: () => number, minWords = 8, maxWords = 20) {\n  const length = minWords + Math.floor(rng() * (maxWords - minWords + 1))\n  const parts: string[] = []\n  for (let i = 0; i < length; i++) {\n    parts.push(pick(rng, WORDS))\n  }\n  return `${capitalize(parts.join(' '))}.`\n}\n\nfunction initialsFromName(name: string) {\n  const parts = name.split(' ')\n  return `${parts[0]?.charAt(0) ?? ''}${parts[1]?.charAt(0) ?? ''}`.toUpperCase()\n}\n\nfunction hueFromText(text: string, salt = 0) {\n  let hash = salt\n  for (let i = 0; i < text.length; i++) {\n    hash = (hash * 31 + text.charCodeAt(i)) % 360\n  }\n  return (hash + 360) % 360\n}\n\nexport function avatarStyle(hue: number) {\n  return {\n    background: `linear-gradient(145deg, hsl(${hue} 68% 44%), hsl(${(hue + 32) % 360} 70% 36%))`,\n  }\n}\n\nexport function createPeopleRows(count: number, withLetters = true, seed = 42) {\n  const rng = createRng(seed)\n  const byLetter = new Map<string, Person[]>()\n\n  for (let i = 0; i < count; i++) {\n    const name = `${pick(rng, FIRST_NAMES)} ${pick(rng, LAST_NAMES)}`\n    const letter = name.charAt(0).toLowerCase()\n    const person: Person = {\n      name,\n      initials: initialsFromName(name),\n      hue: hueFromText(name),\n    }\n    const bucket = byLetter.get(letter) ?? []\n    bucket.push(person)\n    byLetter.set(letter, bucket)\n  }\n\n  const rows: PersonRow[] = []\n  const letters = 'abcdefghijklmnopqrstuvwxyz'.split('')\n  let index = 0\n  let id = 1\n\n  for (const letter of letters) {\n    const bucket = (byLetter.get(letter) ?? []).sort((a, b) => a.name.localeCompare(b.name))\n    if (!bucket.length)\n      continue\n\n    if (withLetters) {\n      rows.push({\n        id: id++,\n        index: index++,\n        type: 'letter',\n        value: letter,\n        height: 96,\n      })\n    }\n\n    for (const person of bucket) {\n      rows.push({\n        id: id++,\n        index: index++,\n        type: 'person',\n        value: person,\n        height: 74,\n      })\n    }\n  }\n\n  return rows\n}\n\nexport function createMessages(count: number, seed = 99) {\n  const rng = createRng(seed)\n  const list: MessageRow[] = []\n\n  for (let i = 0; i < count; i++) {\n    const user = `${pick(rng, FIRST_NAMES)} ${pick(rng, LAST_NAMES)}`\n    const timestamp = `${String(8 + Math.floor((i % 720) / 60)).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`\n    list.push({\n      id: i + 1,\n      user,\n      initials: initialsFromName(user),\n      hue: hueFromText(user, i),\n      message: `${sentence(rng)} ${rng() > 0.5 ? sentence(rng, 4, 11) : ''}`.trim(),\n      timestamp,\n    })\n  }\n\n  return list\n}\n\nexport function mutateMessage(row: MessageRow, seed = 1234) {\n  const rng = createRng(seed + row.id)\n  row.message = `${sentence(rng, 5, 14)} ${sentence(rng, 8, 18)}`\n}\n\nexport function createSimpleStrings(count: number, seed = 7) {\n  const rng = createRng(seed)\n  const list: string[] = []\n  for (let i = 0; i < count; i++) {\n    list.push(`${sentence(rng, 5, 14)} ${rng() > 0.6 ? sentence(rng, 4, 10) : ''}`.trim())\n  }\n  return list\n}\n\nconst GRADIENTS = [\n  'linear-gradient(145deg, #57cc99, #2d6a4f)',\n  'linear-gradient(145deg, #ff8fa3, #7b2cbf)',\n  'linear-gradient(145deg, #56cfe1, #4361ee)',\n  'linear-gradient(145deg, #ffd166, #f77f00)',\n  'linear-gradient(145deg, #52b788, #1b4332)',\n  'linear-gradient(145deg, #f8961e, #f94144)',\n]\n\nexport function gradientAt(index: number) {\n  return GRADIENTS[index % GRADIENTS.length]\n}\n"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "content": "import { defineConfig } from 'vitepress'\n\nexport default defineConfig({\n  title: 'Vue Virtual Scroller',\n  description: 'Blazing fast scrolling of any amount of data',\n\n  themeConfig: {\n    nav: [\n      { text: 'Guide', link: '/guide/' },\n      { text: 'Demos', link: '/demos/' },\n      {\n        text: 'Links',\n        items: [\n          { text: 'Live Demo', link: 'https://vue-virtual-scroller-demo.netlify.app/' },\n          { text: 'GitHub', link: 'https://github.com/Akryum/vue-virtual-scroller' },\n          { text: 'Changelog', link: 'https://github.com/Akryum/vue-virtual-scroller/blob/master/CHANGELOG.md' },\n        ],\n      },\n    ],\n\n    sidebar: {\n      '/guide/': [\n        {\n          text: 'Introduction',\n          items: [\n            { text: 'Getting Started', link: '/guide/' },\n          ],\n        },\n        {\n          text: 'Components',\n          items: [\n            { text: 'RecycleScroller', link: '/guide/recycle-scroller' },\n            { text: 'DynamicScroller', link: '/guide/dynamic-scroller' },\n            { text: 'DynamicScrollerItem', link: '/guide/dynamic-scroller-item' },\n          ],\n        },\n        {\n          text: 'Utilities',\n          items: [\n            { text: 'IdState', link: '/guide/id-state' },\n            { text: 'Headless (useRecycleScroller)', link: '/guide/use-recycle-scroller' },\n          ],\n        },\n        { text: 'AI & Skills', link: '/guide/ai-skills' },\n      ],\n      '/demos/': [\n        {\n          text: 'Demos',\n          items: [\n            { text: 'Overview', link: '/demos/' },\n            { text: 'RecycleScroller', link: '/demos/recycle-scroller' },\n            { text: 'DynamicScroller', link: '/demos/dynamic-scroller' },\n            { text: 'Chat Stream', link: '/demos/chat' },\n            { text: 'Simple List', link: '/demos/simple-list' },\n            { text: 'Horizontal', link: '/demos/horizontal' },\n            { text: 'Grid', link: '/demos/grid' },\n            { text: 'Test Chat', link: '/demos/test-chat' },\n          ],\n        },\n      ],\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/Akryum/vue-virtual-scroller' },\n    ],\n\n    footer: {\n      message: 'Released under the MIT License.',\n      copyright: 'Copyright Akryum',\n    },\n\n    search: {\n      provider: 'local',\n    },\n  },\n})\n"
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "content": "import DefaultTheme from 'vitepress/theme'\nimport './style.css'\n\nexport default {\n  extends: DefaultTheme,\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/style.css",
    "content": ":root {\n  --demo-bg: linear-gradient(145deg, #f4f8f2 0%, #edf7f6 48%, #f8f2e9 100%);\n  --demo-surface: rgba(255, 255, 255, 0.86);\n  --demo-border: rgba(47, 73, 59, 0.16);\n  --demo-accent: #2f7a52;\n  --demo-accent-soft: rgba(47, 122, 82, 0.12);\n  --demo-text: #173224;\n  --demo-muted: #4c6a5c;\n  --demo-shadow: 0 12px 35px rgba(13, 42, 29, 0.12);\n}\n\n.demo-shell {\n  border: 1px solid var(--demo-border);\n  border-radius: 18px;\n  background: var(--demo-bg);\n  box-shadow: var(--demo-shadow);\n  overflow: hidden;\n  margin: 18px 0 26px;\n}\n\n.demo-shell__header {\n  padding: 18px 20px;\n  border-bottom: 1px solid var(--demo-border);\n  background: linear-gradient(180deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.55));\n}\n\n.demo-shell__title {\n  margin: 0;\n  color: var(--demo-text);\n  font-family: 'Avenir Next', 'Helvetica Neue', 'Segoe UI', sans-serif;\n  font-size: 1.05rem;\n  letter-spacing: 0.01em;\n}\n\n.demo-shell__description {\n  margin: 6px 0 0;\n  color: var(--demo-muted);\n  font-size: 0.92rem;\n  line-height: 1.45;\n}\n\n.demo-shell__toolbar {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n  padding: 12px 18px;\n  border-bottom: 1px solid var(--demo-border);\n  background: rgba(255, 255, 255, 0.52);\n}\n\n.demo-shell__viewport {\n  padding: 16px;\n}\n\n.demo-chip {\n  display: inline-flex;\n  align-items: center;\n  height: 34px;\n  gap: 8px;\n  border-radius: 999px;\n  padding: 0 12px;\n  color: var(--demo-text);\n  background: var(--demo-surface);\n  border: 1px solid var(--demo-border);\n  box-shadow: 0 3px 10px rgba(13, 42, 29, 0.06);\n  font-size: 0.84rem;\n}\n\n.demo-chip input,\n.demo-chip button,\n.demo-chip select {\n  border: 0;\n  outline: 0;\n  background: transparent;\n  color: inherit;\n  font: inherit;\n}\n\n.demo-chip input[type=\"number\"],\n.demo-chip input[type=\"text\"] {\n  width: 78px;\n}\n\n.demo-chip input[type=\"range\"] {\n  width: 110px;\n}\n\n.demo-button {\n  border: 1px solid color-mix(in srgb, var(--demo-accent) 70%, #fff 30%);\n  background: linear-gradient(180deg, #4ba774, #2f7a52);\n  color: #fff;\n  border-radius: 999px;\n  padding: 4px 12px;\n  cursor: pointer;\n  font-size: 0.82rem;\n  font-weight: 600;\n}\n\n.demo-button.secondary {\n  background: #fff;\n  color: var(--demo-accent);\n  border-color: rgba(47, 122, 82, 0.35);\n}\n\n.demo-viewport {\n  height: 560px;\n  border-radius: 14px;\n  background: rgba(255, 255, 255, 0.6);\n  border: 1px solid var(--demo-border);\n  overflow: hidden;\n}\n\n.demo-person-row {\n  min-height: 74px;\n  display: grid;\n  grid-template-columns: 52px 1fr auto;\n  gap: 12px;\n  align-items: center;\n  padding: 10px 16px;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n}\n\n.demo-avatar {\n  width: 44px;\n  height: 44px;\n  border-radius: 50%;\n  display: grid;\n  place-items: center;\n  color: #fff;\n  font-size: 0.92rem;\n  font-weight: 700;\n}\n\n.demo-letter-row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 0 16px;\n  background: linear-gradient(90deg, rgba(47, 122, 82, 0.16), rgba(47, 122, 82, 0.04));\n  border-bottom: 1px solid rgba(47, 122, 82, 0.14);\n  cursor: pointer;\n}\n\n.demo-letter-row strong {\n  font-size: 1.35rem;\n  text-transform: uppercase;\n  color: #214935;\n}\n\n.demo-message-row {\n  display: grid;\n  grid-template-columns: 42px 1fr auto;\n  gap: 10px;\n  align-items: flex-start;\n  padding: 10px 14px;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n  cursor: pointer;\n}\n\n.demo-message-body {\n  color: #1f352a;\n  line-height: 1.4;\n}\n\n.demo-message-meta {\n  color: #6a7f74;\n  font-size: 0.78rem;\n  white-space: nowrap;\n}\n\n.demo-notice {\n  margin: 10px;\n  padding: 10px 12px;\n  border-radius: 10px;\n  border: 1px dashed rgba(47, 122, 82, 0.35);\n  color: #2e5c43;\n  background: var(--demo-accent-soft);\n}\n\n.demo-grid-card {\n  height: 100%;\n  border-radius: 14px;\n  border: 1px solid rgba(0, 0, 0, 0.08);\n  color: #fff;\n  padding: 12px;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);\n}\n\n.demo-horizontal-track {\n  height: 320px;\n}\n\n.demo-horizontal-card {\n  height: 100%;\n  border-right: 1px solid rgba(0, 0, 0, 0.08);\n  padding: 12px;\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n  background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.5));\n}\n\n.demo-chat-bubble {\n  border-radius: 14px;\n  padding: 10px 12px;\n  background: rgba(255, 255, 255, 0.92);\n  border: 1px solid rgba(0, 0, 0, 0.06);\n}\n\n@media (max-width: 760px) {\n  .demo-shell__toolbar {\n    gap: 8px;\n    padding: 10px;\n  }\n\n  .demo-chip {\n    width: 100%;\n    justify-content: space-between;\n  }\n\n  .demo-viewport {\n    height: 470px;\n  }\n}\n"
  },
  {
    "path": "docs/demos/chat.md",
    "content": "<script setup>\nimport ChatStreamDocDemo from '../.vitepress/components/demos/ChatStreamDocDemo.vue'\n</script>\n\n# Chat Stream Demo\n\nUse this demo for chat, logs, and live feeds that continuously append data.\n\nWhat to try:\n\n- Start/stop the stream and observe scroll stability.\n- Append large batches (`+20 messages`) to validate throughput.\n- Apply filters while data is growing.\n- Confirm the list stays pinned near the latest items.\n\n<ChatStreamDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue'\nimport { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'\nimport { createMessages } from '../.vitepress/components/demos/demo-data'\n\nconst scroller = ref<InstanceType<typeof DynamicScroller>>()\nconst basePool = createMessages(1500, 303)\n\nlet nextId = 1\nconst stream = ref(createMessages(20, 707).map(item => ({ ...item, id: nextId++ })))\nconst search = ref('')\nconst streaming = ref(false)\n\nlet streamTimer: ReturnType<typeof setInterval> | undefined\n\nconst filteredItems = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return stream.value\n  return stream.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))\n})\n\nfunction appendBatch(amount = 8) {\n  for (let i = 0; i < amount; i++) {\n    const template = basePool[(nextId + i) % basePool.length]\n    stream.value.push({\n      ...template,\n      id: nextId++,\n      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),\n    })\n  }\n  requestAnimationFrame(() => scroller.value?.scrollToBottom())\n}\n\nfunction startStream() {\n  if (streaming.value)\n    return\n  streaming.value = true\n  appendBatch(12)\n  streamTimer = setInterval(() => {\n    appendBatch(6)\n  }, 320)\n}\n\nfunction stopStream() {\n  streaming.value = false\n  if (streamTimer) {\n    clearInterval(streamTimer)\n    streamTimer = undefined\n  }\n}\n\nonBeforeUnmount(stopStream)\n</script>\n\n<template>\n  <DynamicScroller\n    ref=\"scroller\"\n    :items=\"filteredItems\"\n    :min-item-size=\"62\"\n  >\n    <template #default=\"{ item, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[item.message]\"\n      >\n        <strong>{{ item.user }}</strong>\n        <p>{{ item.message }}</p>\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/dynamic-scroller.md",
    "content": "<script setup>\nimport DynamicScrollerDocDemo from '../.vitepress/components/demos/DynamicScrollerDocDemo.vue'\n</script>\n\n# DynamicScroller Demo\n\nUse this demo when item height is not known ahead of time.\n\nWhat to try:\n\n- Filter the list to verify virtualization still behaves correctly.\n- Click messages to mutate content and trigger automatic re-measurement.\n- Adjust `Min row size` to see first-render tradeoffs.\n- Watch the visible range to understand viewport updates.\n\n<DynamicScrollerDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'\nimport { createMessages, mutateMessage } from '../.vitepress/components/demos/demo-data'\n\nconst search = ref('')\nconst messages = ref(createMessages(600, 101))\nconst minItemSize = ref(68)\n\nconst filteredMessages = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return messages.value\n  return messages.value.filter(item => item.message.toLowerCase().includes(term) || item.user.toLowerCase().includes(term))\n})\n\nfunction randomizeMessage(index: number) {\n  const row = filteredMessages.value[index]\n  if (!row)\n    return\n  mutateMessage(row, Date.now() % 997)\n}\n</script>\n\n<template>\n  <DynamicScroller\n    :items=\"filteredMessages\"\n    :min-item-size=\"minItemSize\"\n  >\n    <template #default=\"{ item, index, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[item.message]\"\n        @click=\"randomizeMessage(index)\"\n      >\n        <strong>{{ item.user }}</strong>\n        <p>{{ item.message }}</p>\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/grid.md",
    "content": "<script setup>\nimport GridDocDemo from '../.vitepress/components/demos/GridDocDemo.vue'\n</script>\n\n# Grid Demo\n\nUse this demo for card galleries and catalog layouts.\n\nWhat to try:\n\n- Change `Items / row` to test responsiveness.\n- Jump to deep indexes with `Scroll to`.\n- Validate performance with thousands of cards.\n\n<GridDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport type { Person } from '../.vitepress/components/demos/demo-data'\nimport { computed, ref } from 'vue'\nimport { RecycleScroller } from 'vue-virtual-scroller'\nimport { createPeopleRows } from '../.vitepress/components/demos/demo-data'\n\ninterface GridCard extends Person {\n  id: number\n}\n\nconst scroller = ref<InstanceType<typeof RecycleScroller>>()\nconst gridItems = ref(5)\nconst scrollTo = ref(300)\n\nconst rawRows = createPeopleRows(2500, false, 111)\n\nconst cards = computed<GridCard[]>(() =>\n  rawRows\n    .filter(row => row.type === 'person')\n    .map((row) => {\n      const person = row.value as Person\n      return {\n        id: row.id,\n        ...person,\n      }\n    }),\n)\n\nfunction jump() {\n  const target = Math.min(Math.max(0, scrollTo.value), cards.value.length - 1)\n  scroller.value?.scrollToItem(target)\n}\n</script>\n\n<template>\n  <RecycleScroller\n    ref=\"scroller\"\n    :items=\"cards\"\n    :item-size=\"166\"\n    :grid-items=\"gridItems\"\n    :item-secondary-size=\"176\"\n  >\n    <template #default=\"{ item }\">\n      <article>\n        <strong>{{ item.initials }}</strong>\n        <span>{{ item.name }}</span>\n      </article>\n    </template>\n  </RecycleScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/horizontal.md",
    "content": "<script setup>\nimport HorizontalDocDemo from '../.vitepress/components/demos/HorizontalDocDemo.vue'\n</script>\n\n# Horizontal Demo\n\nUse this demo for horizontally scrolling lists with dynamic item width.\n\nWhat to try:\n\n- Scroll horizontally with trackpad or Shift + mouse wheel.\n- Filter cards and verify smooth reflow.\n- Inspect how variable-width content stays virtualized.\n\n<HorizontalDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'\nimport { createMessages } from '../.vitepress/components/demos/demo-data'\n\nconst search = ref('')\nconst rows = ref(createMessages(500, 909))\n\nconst filteredRows = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return rows.value\n  return rows.value.filter(row => row.message.toLowerCase().includes(term) || row.user.toLowerCase().includes(term))\n})\n\nfunction cardWidth(message: string) {\n  return Math.max(180, Math.min(440, Math.round(message.length * 0.95)))\n}\n</script>\n\n<template>\n  <DynamicScroller\n    :items=\"filteredRows\"\n    :min-item-size=\"180\"\n    direction=\"horizontal\"\n  >\n    <template #default=\"{ item, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[item.message]\"\n        :style=\"{ width: `${cardWidth(item.message)}px` }\"\n      >\n        {{ item.message }}\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/index.md",
    "content": "# Demos\n\nInteractive demos for common real-world use cases.\n\n## Pick a demo\n\n- [RecycleScroller demo](./recycle-scroller) — fixed or variable-size rows with large datasets.\n- [DynamicScroller demo](./dynamic-scroller) — unknown row heights with live size recalculation.\n- [Chat stream demo](./chat) — append-only feeds with auto-scroll to bottom.\n- [Simple list demo](./simple-list) — compare `DynamicScroller` and `RecycleScroller` quickly.\n- [Horizontal demo](./horizontal) — dynamic-size cards in horizontal direction.\n- [Grid demo](./grid) — multi-column virtualized layouts.\n- [Test chat demo](./test-chat) — stress-test frequent insertions and bottom pinning.\n"
  },
  {
    "path": "docs/demos/recycle-scroller.md",
    "content": "<script setup>\nimport RecycleScrollerDocDemo from '../.vitepress/components/demos/RecycleScrollerDocDemo.vue'\n</script>\n\n# RecycleScroller Demo\n\nUse this demo when your list items have known sizes, or when sizes can be provided by data.\n\nWhat to try:\n\n- Change `Items` to simulate very large datasets.\n- Toggle `Variable height` and click letter rows to see size updates.\n- Tune `Buffer` to understand render-ahead behavior.\n- Use `Jump` to test `scrollToItem`.\n\n<RecycleScrollerDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { RecycleScroller } from 'vue-virtual-scroller'\nimport { createPeopleRows } from '../.vitepress/components/demos/demo-data'\n\nconst count = ref(8000)\nconst withLetters = ref(true)\nconst buffer = ref(240)\nconst rows = ref([])\n\nconst itemSize = computed(() => (withLetters.value ? null : 74))\n\nfunction regenerate() {\n  rows.value = createPeopleRows(Math.max(50, count.value), withLetters.value, 17)\n}\n\nfunction toggleLetterSize(row: any) {\n  if (row.type === 'letter') {\n    row.height = row.height === 96 ? 136 : 96\n  }\n}\n\nwatch([count, withLetters], regenerate)\nonMounted(regenerate)\n</script>\n\n<template>\n  <RecycleScroller\n    :items=\"rows\"\n    :item-size=\"itemSize\"\n    :buffer=\"buffer\"\n    key-field=\"id\"\n    size-field=\"height\"\n  >\n    <template #default=\"{ item, index }\">\n      <div\n        v-if=\"item.type === 'letter'\"\n        :style=\"{ height: `${item.height}px` }\"\n        @click=\"toggleLetterSize(item)\"\n      >\n        <strong>{{ item.value }}</strong> ({{ index }})\n      </div>\n      <div\n        v-else\n        :style=\"{ height: `${item.height}px` }\"\n      >\n        {{ item.value.name }}\n      </div>\n    </template>\n  </RecycleScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/simple-list.md",
    "content": "<script setup>\nimport SimpleListDocDemo from '../.vitepress/components/demos/SimpleListDocDemo.vue'\n</script>\n\n# Simple List Demo\n\nUse this demo to compare dynamic and fixed-size strategies on the same dataset.\n\nWhat to try:\n\n- Toggle `Dynamic mode` on/off to compare behavior.\n- Filter the list and compare how both modes respond.\n- Use this as a reference when deciding between `DynamicScroller` and `RecycleScroller`.\n\n<SimpleListDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { DynamicScroller, DynamicScrollerItem, RecycleScroller } from 'vue-virtual-scroller'\nimport { createSimpleStrings } from '../.vitepress/components/demos/demo-data'\n\nconst useDynamic = ref(true)\nconst search = ref('')\nconst rows = ref(createSimpleStrings(4000, 505))\n\nconst filteredRows = computed(() => {\n  const term = search.value.trim().toLowerCase()\n  if (!term)\n    return rows.value\n  return rows.value.filter(item => item.toLowerCase().includes(term))\n})\n</script>\n\n<template>\n  <DynamicScroller\n    v-if=\"useDynamic\"\n    :items=\"filteredRows\"\n    :min-item-size=\"58\"\n  >\n    <template #default=\"{ item, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[item]\"\n      >\n        {{ item }}\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n\n  <RecycleScroller\n    v-else\n    :items=\"filteredRows\"\n    :item-size=\"58\"\n  >\n    <template #default=\"{ item }\">\n      {{ item }}\n    </template>\n  </RecycleScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/demos/test-chat.md",
    "content": "<script setup>\nimport TestChatDocDemo from '../.vitepress/components/demos/TestChatDocDemo.vue'\n</script>\n\n# Test Chat Demo\n\nUse this demo to test append-heavy timelines and quick burst updates.\n\nWhat to try:\n\n- Add rows in different batch sizes (`+1`, `+5`, `+20`, `+80`).\n- Confirm auto-scroll behavior under repeated inserts.\n- Use it as a sanity check for real-time message UIs.\n\n<TestChatDocDemo />\n\n\n## Relevant source code\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'\nimport { createSimpleStrings } from '../.vitepress/components/demos/demo-data'\n\nconst pool = createSimpleStrings(1200, 1303)\nconst scroller = ref<InstanceType<typeof DynamicScroller>>()\nconst rows = ref<{ id: number, text: string }[]>([])\n\nlet nextId = 1\n\nfunction addItems(count = 1) {\n  for (let i = 0; i < count; i++) {\n    rows.value.push({\n      id: nextId,\n      text: pool[nextId % pool.length],\n    })\n    nextId++\n  }\n  requestAnimationFrame(() => scroller.value?.scrollToBottom())\n}\n</script>\n\n<template>\n  <DynamicScroller\n    ref=\"scroller\"\n    :items=\"rows\"\n    :min-item-size=\"48\"\n    @resize=\"scroller?.scrollToBottom()\"\n  >\n    <template #default=\"{ item, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[item.text]\"\n      >\n        {{ item.text }}\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n</template>\n```\n"
  },
  {
    "path": "docs/guide/ai-skills.md",
    "content": "# AI & Skills\n\nIf you use AI coding agents, `vue-virtual-scroller` ships a package skill that can be discovered from the installed npm package.\n\n## One-off usage with `npx skills-npm`\n\nAfter installing `vue-virtual-scroller` in your project:\n\n```bash\npnpm add vue-virtual-scroller@next\nnpx skills-npm\n```\n\nThis lets supported coding agents discover the skill that ships inside the package.\n\n## Repeatable setup\n\nIf you want skill links to refresh automatically after installs:\n\n```bash\nnpm i -D skills-npm\n```\n\nAdd a `prepare` script in your project:\n\n```json\n{\n  \"scripts\": {\n    \"prepare\": \"skills-npm\"\n  }\n}\n```\n\n## Useful options\n\n- `--source <source>` chooses `package.json` or `node_modules`\n- `--cwd <cwd>` targets a specific workspace root\n- `--recursive` scans monorepos\n- `--dry-run` previews the generated links\n- `--yes` skips prompts\n\nFor more control, create a `skills-npm.config.ts` file in your consumer project.\n\nLearn more about `skills-npm` [here](https://github.com/antfu/skills-npm#skills-npm).\n\n## Notes\n\n- Run `skills-npm` from the consumer project root, not from this package repository.\n- Generated links are typically local setup artifacts. Add `skills/npm-*` to `.gitignore` if you do not want them committed.\n- The published `vue-virtual-scroller` package includes its `skills/` directory so discovery tools can find the shipped skill.\n"
  },
  {
    "path": "docs/guide/dynamic-scroller-item.md",
    "content": "# DynamicScrollerItem\n\nThe component that should wrap all the items in a [DynamicScroller](./dynamic-scroller) to handle size computations.\n\n## Props\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `item` (required) | — | The item rendered in the scroller. |\n| `active` (required) | — | Is the holding view active in RecycleScroller. Will prevent unnecessary size recomputation. |\n| `sizeDependencies` | — | Values that can affect the size of the item. This prop will be watched and if one value changes, the size will be recomputed. Recommended instead of `watchData`. |\n| `watchData` | `false` | Deeply watch `item` for changes to re-calculate the size (not recommended, can impact performance). |\n| `tag` | `'div'` | Element used to render the component. |\n| `emitResize` | `false` | Emit the `resize` event each time the size is recomputed (can impact performance). |\n\n## Events\n\n| Event | Description |\n|-------|-------------|\n| `resize` | Emitted each time the size is recomputed, only if `emitResize` prop is `true`. |\n"
  },
  {
    "path": "docs/guide/dynamic-scroller.md",
    "content": "# DynamicScroller\n\nThis works just like the [RecycleScroller](./recycle-scroller), but it can render items with unknown sizes!\n\n## Basic usage\n\n```vue\n<script>\nexport default {\n  props: {\n    items: Array,\n  },\n}\n</script>\n\n<template>\n  <DynamicScroller\n    :items=\"items\"\n    :min-item-size=\"54\"\n    class=\"scroller\"\n  >\n    <template #default=\"{ item, index, active }\">\n      <DynamicScrollerItem\n        :item=\"item\"\n        :active=\"active\"\n        :size-dependencies=\"[\n          item.message,\n        ]\"\n        :data-index=\"index\"\n      >\n        <div class=\"avatar\">\n          <img\n            :key=\"item.avatar\"\n            :src=\"item.avatar\"\n            alt=\"avatar\"\n            class=\"image\"\n          >\n        </div>\n        <div class=\"text\">\n          {{ item.message }}\n        </div>\n      </DynamicScrollerItem>\n    </template>\n  </DynamicScroller>\n</template>\n\n<style scoped>\n.scroller {\n  height: 100%;\n}\n</style>\n```\n\n## Important notes\n\n- `minItemSize` is required for the initial render of items.\n- `DynamicScroller` won't detect size changes on its own, but you can put values that can affect the item size with `size-dependencies` on [DynamicScrollerItem](./dynamic-scroller-item).\n- You don't need to have a `size` field on the items.\n\n## Props\n\nExtends all the [RecycleScroller props](./recycle-scroller#props).\n\n::: tip\nIt's not recommended to change the `sizeField` prop since all the size management is done internally.\n:::\n\n## Events\n\nExtends all the [RecycleScroller events](./recycle-scroller#events).\n\n## Default scoped slot props\n\nExtends all the [RecycleScroller scoped slot props](./recycle-scroller#default-scoped-slot-props).\n\n## Other slots\n\nExtends all the [RecycleScroller other slots](./recycle-scroller#other-slots).\n"
  },
  {
    "path": "docs/guide/id-state.md",
    "content": "# IdState\n\nThis is a convenience mixin that can replace `data` in components being rendered in a [RecycleScroller](./recycle-scroller).\n\n## Why is this useful?\n\nSince the components in RecycleScroller are reused, you can't directly use the Vue standard `data` properties: otherwise they will be shared with different items in the list!\n\nIdState will instead provide an `idState` object which is equivalent to `$data`, but it's linked to a single item with its identifier (you can change which field with `idProp` param).\n\n## Example\n\nIn this example, we use the `id` of the `item` to have a \"scoped\" state to the item:\n\n```vue\n<script>\nimport { IdState } from 'vue-virtual-scroller'\n\nexport default {\n  mixins: [\n    IdState({\n      // You can customize this\n      idProp: vm => vm.item.id,\n    }),\n  ],\n\n  props: {\n    // Item in the list\n    item: Object,\n  },\n\n  // This replaces data () { ... }\n  idState() {\n    return {\n      replyOpen: false,\n      replyText: '',\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"question\">\n    <p>{{ item.question }}</p>\n    <button @click=\"idState.replyOpen = !idState.replyOpen\">\n      Reply\n    </button>\n    <textarea\n      v-if=\"idState.replyOpen\"\n      v-model=\"idState.replyText\"\n      placeholder=\"Type your reply\"\n    />\n  </div>\n</template>\n```\n\n## Parameters\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `idProp` | `vm => vm.item.id` | Field name on the component (for example: `'id'`) or function returning the id. |\n"
  },
  {
    "path": "docs/guide/index.md",
    "content": "# Getting Started\n\n<div class=\"badges\">\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg)](https://npmx.dev/package/vue-virtual-scroller)\n[![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://npmx.dev/package/vue-virtual-scroller)\n[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)\n\n</div>\n\nBlazing fast scrolling of any amount of data | [Demos](../demos/index.md) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)\n\nFor Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller).\n\n## Installation\n\n```sh\nnpm install vue-virtual-scroller@next\n```\n\n```sh\nyarn add vue-virtual-scroller@next\n```\n\n```sh\npnpm add vue-virtual-scroller@next\n```\n\n## Setup\n\n`vue-virtual-scroller` ships ESM only. Use it from an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.\n\n### Plugin import\n\nInstall all the components:\n\n```js\nimport VueVirtualScroller from 'vue-virtual-scroller'\n\napp.use(VueVirtualScroller)\n```\n\nUse specific components:\n\n```js\nimport { RecycleScroller } from 'vue-virtual-scroller'\n\napp.component('RecycleScroller', RecycleScroller)\n```\n\n::: warning\nThe CSS file must be imported when using the package:\n\n```js\nimport 'vue-virtual-scroller/index.css'\n```\n:::\n\n## Components\n\nThere are several components provided by `vue-virtual-scroller`:\n\n- [**RecycleScroller**](./recycle-scroller) — only renders the visible items in your list. It also re-uses components and DOM elements to be as efficient and performant as possible.\n\n- [**DynamicScroller**](./dynamic-scroller) — wraps RecycleScroller and extends its features to include dynamic size management. The main use case is when you **do not know the size of the items** in advance. It automatically \"discovers\" item dimensions as it renders new items during scrolling.\n\n- [**DynamicScrollerItem**](./dynamic-scroller-item) — must wrap each item in a DynamicScroller to handle size computations.\n\n- [**IdState**](./id-state) — a mixin that eases local state management in reused components inside a RecycleScroller.\n\n- [**useRecycleScroller (headless)**](./use-recycle-scroller) — low-level composable API to build your own virtual scroller UI without using bundled components.\n\n<style scoped>\n.badges p {\n  display: flex;\n  gap: 0.5rem;\n  flex-wrap: wrap;\n}\n</style>\n"
  },
  {
    "path": "docs/guide/recycle-scroller.md",
    "content": "# RecycleScroller\n\nRecycleScroller is a virtual scroller that only renders the visible items. As the user scrolls, RecycleScroller reuses all components and DOM nodes to maintain optimal performance.\n\n## Basic usage\n\nUse the scoped slot to render each item in the list:\n\n```vue\n<script>\nexport default {\n  props: {\n    list: Array,\n  },\n}\n</script>\n\n<template>\n  <RecycleScroller\n    v-slot=\"{ item }\"\n    class=\"scroller\"\n    :items=\"list\"\n    :item-size=\"32\"\n    key-field=\"id\"\n  >\n    <div class=\"user\">\n      {{ item.name }}\n    </div>\n  </RecycleScroller>\n</template>\n\n<style scoped>\n.scroller {\n  height: 100%;\n}\n\n.user {\n  height: 32%;\n  padding: 0 12px;\n  display: flex;\n  align-items: center;\n}\n</style>\n```\n\n## Important notes\n\n::: warning\nYou need to set the size of the virtual-scroller element and the items elements (for example, with CSS). Unless you are using [variable size mode](#variable-size-mode), all items should have the same height (or width in horizontal mode) to prevent display glitches.\n:::\n\n::: warning\nIf the items are objects, the scroller needs to be able to identify them. By default it will look for an `id` field on the items. This can be configured with the `keyField` prop if you are using another field name.\n:::\n\n- It is not recommended to use functional components inside RecycleScroller since the components are reused (so it will actually be slower).\n- The list item components must be reactive to the `item` prop being updated without being re-created (use computed props or watchers to properly react to props changes!).\n- You don't need to set `key` on list content (but you should on all nested `<img>` elements to prevent load glitches).\n- The browsers have a size limitation on DOM elements, it means that currently the virtual scroller can't display more than ~500k items depending on the browser.\n- Since DOM elements are reused for items, it's recommended to define hover styles using the provided `hover` class instead of the `:hover` state selector (e.g. `.vue-recycle-scroller__item-view.hover` or `.hover .some-element-inside-the-item-view`).\n\n## How does it work?\n\n- The RecycleScroller creates pools of views to render visible items to the user.\n- A view holds a rendered item, and is reused inside its pool.\n- For each type of item, a new pool is created so that the same components (and DOM trees) are reused for the same type.\n- Views can be deactivated if they go off-screen, and can be reused anytime for a newly visible item.\n\nHere is what the internals of RecycleScroller look like in vertical mode:\n\n```html\n<RecycleScroller>\n  <!-- Wrapper element with a pre-calculated total height -->\n  <wrapper\n    :style=\"{ height: computedTotalHeight + 'px' }\"\n  >\n    <!-- Each view is translated to the computed position -->\n    <view\n      v-for=\"view of pool\"\n      :style=\"{ transform: 'translateY(' + view.computedTop + 'px)' }\"\n    >\n      <!-- Your elements will be rendered here -->\n      <slot\n        :item=\"view.item\"\n        :index=\"view.nr.index\"\n        :active=\"view.nr.used\"\n      />\n    </view>\n  </wrapper>\n</RecycleScroller>\n```\n\nWhen the user scrolls inside RecycleScroller, the views are mostly just moved around to fill the new visible space, and the default slot properties updated. That way we get the minimum amount of components/elements creation and destruction and we use the full power of Vue virtual-dom diff algorithm to optimize DOM operations!\n\n## Props\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `items` | — | List of items you want to display in the scroller. |\n| `direction` | `'vertical'` | Scrolling direction, either `'vertical'` or `'horizontal'`. |\n| `itemSize` | `null` | Display height (or width in horizontal mode) of the items in pixels used to calculate the scroll size and position. If set to `null`, it will use [variable size mode](#variable-size-mode). |\n| `gridItems` | — | Display that many items on the same line to create a grid. You must set `itemSize` to use this prop (dynamic sizes are not supported). |\n| `itemSecondarySize` | — | Size in pixels (width in vertical mode, height in horizontal mode) of the items in the grid when `gridItems` is set. Defaults to `itemSize` if not set. |\n| `minItemSize` | — | Minimum size used if the height (or width in horizontal mode) of an item is unknown. |\n| `sizeField` | `'size'` | Field used to get the item's size in variable size mode. |\n| `typeField` | `'type'` | Field used to differentiate different kinds of components in the list. For each distinct type, a pool of recycled items will be created. |\n| `keyField` | `'id'` | Field used to identify items and optimize managing rendered views. |\n| `pageMode` | `false` | Enable [Page mode](#page-mode). |\n| `prerender` | `0` | Render a fixed number of items for Server-Side Rendering (SSR). |\n| `buffer` | `200` | Amount of pixels to add to edges of the scrolling visible area to start rendering items further away. |\n| `emitUpdate` | `false` | Emit an `'update'` event each time the virtual scroller content is updated (can impact performance). |\n| `updateInterval` | `0` | The interval in ms at which the view will be checked for updates after scrolling. When set to `0`, check happens during the next animation frame. |\n| `listClass` | `''` | Custom classes added to the item list wrapper. |\n| `itemClass` | `''` | Custom classes added to each item. |\n| `listTag` | `'div'` | The element to render as the list's wrapper. |\n| `itemTag` | `'div'` | The element to render as the list item (the direct parent of the default slot content). |\n\n## Events\n\n| Event | Description |\n|-------|-------------|\n| `resize` | Emitted when the size of the scroller changes. |\n| `visible` | Emitted when the scroller considers itself to be visible in the page. |\n| `hidden` | Emitted when the scroller is hidden in the page. |\n| `update(startIndex, endIndex, visibleStartIndex, visibleEndIndex)` | Emitted each time the views are updated, only if `emitUpdate` prop is `true`. |\n| `scroll-start` | Emitted when the first item is rendered. |\n| `scroll-end` | Emitted when the last item is rendered. |\n\n## Default scoped slot props\n\n| Prop | Description |\n|------|-------------|\n| `item` | Item being rendered in a view. |\n| `index` | Reflects each item's position in the `items` array. |\n| `active` | Whether or not the view is active. An active view is considered visible and being positioned by RecycleScroller. An inactive view is not considered visible and is hidden from the user. Any rendering-related computations should be skipped if the view is inactive. |\n\n## Other slots\n\nThe `empty` slot is displayed only when `items` is empty.\n\n```html\n<main>\n  <slot name=\"before\"></slot>\n  <wrapper>\n    <!-- Reused view pools here -->\n    <slot name=\"empty\"></slot>\n  </wrapper>\n  <slot name=\"after\"></slot>\n</main>\n```\n\nExample:\n\n```vue\n<RecycleScroller\n  class=\"scroller\"\n  :items=\"list\"\n  :item-size=\"32\"\n>\n  <template #before>\n    Hey! I'm a message displayed before the items!\n  </template>\n\n  <template v-slot=\"{ item }\">\n    <div class=\"user\">\n      {{ item.name }}\n    </div>\n  </template>\n</RecycleScroller>\n```\n\n## Page mode\n\nThe page mode expands the virtual-scroller and uses the page viewport to compute which items are visible. That way, you can use it in a big page with HTML elements before or after (like a header and a footer). Set the `page-mode` prop to `true`:\n\n```html\n<header>\n  <menu></menu>\n</header>\n\n<RecycleScroller page-mode>\n  <!-- ... -->\n</RecycleScroller>\n\n<footer>\n  Copyright 2017 - Cat\n</footer>\n```\n\n## Variable size mode\n\n::: warning\nThis mode can be performance heavy with a lot of items. Use with caution.\n:::\n\nIf the `itemSize` prop is not set or is set to `null`, the virtual scroller will switch to variable size mode. You then need to expose a number field on the item objects with the size of the item element.\n\n::: warning\nYou still need to set the size of the items with CSS correctly (with classes for example).\n:::\n\nUse the `sizeField` prop (default is `'size'`) to set the field used by the scroller to get the size for each item.\n\nExample:\n\n```js\nconst items = [\n  {\n    id: 1,\n    label: 'Title',\n    size: 64,\n  },\n  {\n    id: 2,\n    label: 'Foo',\n    size: 32,\n  },\n  {\n    id: 3,\n    label: 'Bar',\n    size: 32,\n  },\n]\n```\n\n## Buffer\n\nYou can set the `buffer` prop (in pixels) on the virtual-scroller to extend the viewport considered when determining the visible items. For example, if you set a buffer of 1000 pixels, the virtual-scroller will start rendering items that are 1000 pixels below the bottom of the scroller visible area, and will keep the items that are 1000 pixels above the top of the visible area.\n\nThe default value is `200`.\n\n```html\n<RecycleScroller :buffer=\"200\" />\n```\n\n## Server-Side Rendering\n\nThe `prerender` prop can be set as the number of items to render on the server inside the virtual scroller:\n\n```html\n<RecycleScroller\n  :items=\"items\"\n  :item-size=\"42\"\n  :prerender=\"10\"\n>\n```\n"
  },
  {
    "path": "docs/guide/use-recycle-scroller.md",
    "content": "# useRecycleScroller (Headless)\n\n`useRecycleScroller` is the low-level composable behind `RecycleScroller`.\n\nUse it when you want full control over markup, styling, and rendering logic while keeping the same virtualization engine.\n\n## When to use this\n\n- You need a custom DOM structure that does not fit the component slot API.\n- You want to integrate virtualization into an existing design system component.\n- You want to control rendering/pooling behavior directly (for example with custom item wrappers).\n\nIf you just need virtual scrolling with standard markup, prefer [`RecycleScroller`](./recycle-scroller).\n\n## Minimal fixed-size example\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useRecycleScroller } from 'vue-virtual-scroller'\n\ninterface User {\n  id: number\n  name: string\n}\n\nconst items = ref<User[]>(\n  Array.from({ length: 10000 }, (_, i) => ({\n    id: i + 1,\n    name: `User ${i + 1}`,\n  })),\n)\n\nconst scrollerEl = ref<HTMLElement>()\n\nconst options = computed(() => ({\n  items: items.value,\n  keyField: 'id',\n  direction: 'vertical' as const,\n  itemSize: 40,\n  gridItems: undefined,\n  itemSecondarySize: undefined,\n  minItemSize: null,\n  sizeField: 'size',\n  typeField: 'type',\n  buffer: 200,\n  pageMode: false,\n  prerender: 0,\n  emitUpdate: false,\n  updateInterval: 0,\n}))\n\nconst {\n  pool,\n  totalSize,\n  handleScroll,\n} = useRecycleScroller(options, scrollerEl)\n</script>\n\n<template>\n  <div\n    ref=\"scrollerEl\"\n    class=\"my-scroller\"\n    @scroll.passive=\"handleScroll\"\n  >\n    <div\n      class=\"my-scroller__inner\"\n      :style=\"{ minHeight: `${totalSize}px` }\"\n    >\n      <div\n        v-for=\"view in pool\"\n        :key=\"view.nr.id\"\n        class=\"my-scroller__item\"\n        :style=\"{ transform: `translateY(${view.position}px)` }\"\n      >\n        <strong>#{{ view.nr.index }}</strong> {{ (view.item as User).name }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.my-scroller {\n  height: 400px;\n  overflow-y: auto;\n  position: relative;\n  border: 1px solid #ddd;\n}\n\n.my-scroller__inner {\n  position: relative;\n  width: 100%;\n  overflow: hidden;\n}\n\n.my-scroller__item {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 40px;\n  display: flex;\n  align-items: center;\n  padding: 0 12px;\n  box-sizing: border-box;\n  border-bottom: 1px solid #f0f0f0;\n}\n</style>\n```\n\n## Required options\n\n`useRecycleScroller` expects the same core options used internally by `RecycleScroller`:\n\n- `items`\n- `keyField`\n- `direction`\n- `itemSize`\n- `minItemSize`\n- `sizeField`\n- `typeField`\n- `buffer`\n- `pageMode`\n- `prerender`\n- `emitUpdate`\n- `updateInterval`\n\nOptional grid options:\n\n- `gridItems`\n- `itemSecondarySize`\n\n## Return values you will use most\n\n- `pool`: visible/recycled views to render.\n- `totalSize`: full virtual size (wrapper min-height/min-width).\n- `handleScroll`: call this on scroll events.\n- `scrollToItem(index)`: programmatic navigation.\n- `scrollToPosition(px)`: absolute scroll positioning.\n- `getScroll()`: current viewport range in pixels.\n- `updateVisibleItems(itemsChanged, checkPositionDiff?)`: force recalculation.\n\n## Variable-size mode\n\nSet `itemSize: null` and provide a numeric field on each item (default `sizeField: 'size'`):\n\n```ts\nconst options = computed(() => ({\n  // ...\n  itemSize: null,\n  minItemSize: 40,\n  sizeField: 'size',\n}))\n```\n\nIn this mode, item objects must expose the size field (`item.size` by default).\n\n## Important notes\n\n- You must provide scrollable sizing styles yourself (`height` or `width` + overflow).\n- Use a stable key field for object items (default: `id`).\n- The composable manages pooling and index mapping, but does not provide built-in markup or CSS.\n- If you need automatic unknown-size measurement, use `DynamicScroller`/`DynamicScrollerItem` (or the `useDynamicScroller` + `useDynamicScrollerItem` composables).\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: home\n\nhero:\n  name: Vue Virtual Scroller\n  tagline: Blazing fast scrolling of any amount of data\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /guide/\n    - theme: alt\n      text: View on GitHub\n      link: https://github.com/Akryum/vue-virtual-scroller\n\nfeatures:\n  - title: Blazing Fast\n    details: Minimal overhead with smart pooling and recycling of views for buttery smooth scrolling.\n    icon: ⚡\n  - title: RecycleScroller\n    details: Only renders visible items and reuses components and DOM elements for optimal performance.\n    icon: ♻️\n  - title: DynamicScroller\n    details: Extends RecycleScroller with dynamic size management for items with unknown sizes.\n    icon: ↕️\n  - title: Headless\n    details: Provides low-level APIs for custom implementations and advanced use cases.\n    icon: 🧠\n---\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu()\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build.environment]\nNODE_VERSION = \"16\"\nNPM_FLAGS = \"--version\" # prevent Netlify npm install\n\n[[redirects]]\nfrom = \"/*\"\nto = \"/index.html\"\nstatus = 200\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vue-virtual-scroller-monorepo\",\n  \"version\": \"2.0.0-beta.10\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af\",\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"scripts\": {\n    \"build\": \"pnpm run -r --filter=!demo build\",\n    \"release\": \"pnpm run lint && pnpm run build && sheep release -b main --force\",\n    \"lint\": \"eslint --cache\",\n    \"test\": \"pnpm run -r test\",\n    \"docs:dev\": \"vitepress dev docs\",\n    \"docs:build\": \"vitepress build docs\",\n    \"docs:preview\": \"vitepress preview docs\"\n  },\n  \"devDependencies\": {\n    \"@akryum/sheep\": \"^0.5.2\",\n    \"@antfu/eslint-config\": \"^7.7.0\",\n    \"eslint\": \"^10.0.2\",\n    \"typescript\": \"^5.3.3\",\n    \"vitepress\": \"^1.6.4\"\n  }\n}\n"
  },
  {
    "path": "packages/demo/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n/tests/e2e/videos/\n/tests/e2e/screenshots/\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "packages/demo/README.md",
    "content": "# vue-virtual-scroller-demos\n\n> Demos for vue-virtual-scroller\n\n## Build Setup\n\n``` bash\n# install dependencies\nyarn install\n\n# serve with hot reload at localhost:8080\nyarn run dev\n\n# build for production with minification\nyarn run build\n```\n"
  },
  {
    "path": "packages/demo/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>vue-virtual-scroller</title>\n    <link rel=\"icon\" href=\"/favicon.png\">\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/demo/package.json",
    "content": "{\n  \"name\": \"demo\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"Demos for vue-virtual-scroller\",\n  \"author\": \"Guillaume Chau <guillaume.b.chau@gmail.com>\",\n  \"scripts\": {\n    \"dev\": \"vite dev --port 8080\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview --port 8080\"\n  },\n  \"dependencies\": {\n    \"@faker-js/faker\": \"^7.6.0\",\n    \"vue\": \"^3.2.41\",\n    \"vue-router\": \"^4.1.5\",\n    \"vue-virtual-scroller\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^3.1.2\",\n    \"vite\": \"^3.1.8\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ]\n}\n"
  },
  {
    "path": "packages/demo/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title>Vue Virtual Scroller Demos</title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "packages/demo/src/App.vue",
    "content": "<template>\n  <nav class=\"menu\">\n    <span class=\"package\">\n      <span class=\"package-name\">vue-virtual-scroller</span>\n    </span>\n\n    <router-link\n      :to=\"{ name: 'home' }\"\n      exact\n    >\n      Home\n    </router-link>\n    <router-link :to=\"{ name: 'recycle' }\">\n      Recycle scroller\n    </router-link>\n    <router-link :to=\"{ name: 'dynamic' }\">\n      Dynamic scroller\n    </router-link>\n    <router-link :to=\"{ name: 'horizontal' }\">\n      Horizontal\n    </router-link>\n    <router-link :to=\"{ name: 'test-chat' }\">\n      Scroll to bottom\n    </router-link>\n    <router-link :to=\"{ name: 'simple-list' }\">\n      Simple array\n    </router-link>\n    <router-link :to=\"{ name: 'chat' }\">\n      Chat demo\n    </router-link>\n    <router-link :to=\"{ name: 'grid' }\">\n      Grid demo\n    </router-link>\n  </nav>\n  <router-view />\n</template>\n\n<style>\nhtml,\nbody,\n#app {\n  box-sizing: border-box;\n  height: 100%;\n}\n\nbody {\n  font-size: 16px;\n  font-family: 'Avenir', Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n  margin: 0;\n}\n\n#app,\n.page {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n}\n\n.menu {\n  flex: auto 0 0;\n  display: flex;\n  align-items: center;\n  gap: 2px;\n}\n\n.menu,\n.page {\n  padding: 12px;\n  box-sizing: border-box;\n}\n\n.package {\n  margin-right: 12px;\n}\n\n.package-name {\n  font-family: monospace;\n  color: #2c3e50;\n  background: #eee;\n  padding: 5px 12px 3px;\n}\n\n.package-name,\n.menu a {\n  display: inline-block;\n  border-radius: 3px;\n}\n\n.menu a {\n  padding: 4px 12px;\n  text-decoration: none;\n  color: white;\n  background: #2c3e50;\n}\n\n.menu a.router-link-active {\n  background: #42b983;\n}\n\n.menu a:not(:last-child) {\n  margin-right: 4px;\n}\n\n.vue-recycle-scroller {\n  -webkit-overflow-scrolling: touch;\n}\n\n.vue-recycle-scroller__item-container,\n.vue-recycle-scroller__item-wrapper {\n  box-sizing: border-box;\n}\n\n.vue-recycle-scroller__item-view {\n  cursor: pointer;\n  user-select: none;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n}\n\n.tr, .td {\n  box-sizing: border-box;\n}\n\n.vue-recycle-scroller__item-view .tr {\n  display: flex;\n  align-items: center;\n}\n\n.vue-recycle-scroller__item-view .td {\n  display: block;\n}\n\n.vue-recycle-scroller__item-view.hover {\n  background: #4fc08d;\n  color: white;\n}\n\n.toolbar {\n  flex: auto 0 0;\n  text-align: center;\n  margin-bottom: 12px;\n  line-height: 32px;\n  position: sticky;\n  top: 0;\n  z-index: 9999;\n  background: white;\n}\n\n.recycle-scroller-demo.page-mode .toolbar {\n  border-bottom: solid 1px #e0edfa;\n}\n\n.toolbar > *:not(:last-child) {\n  margin-right: 24px;\n}\n\n.avatar {\n  background: grey;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/ChatDemo.vue",
    "content": "<script>\nimport { generateMessage } from '../data'\n\nlet id = 0\n\nconst messages = []\nfor (let i = 0; i < 10000; i++) {\n  messages.push(generateMessage())\n}\n\nexport default {\n  data() {\n    return {\n      items: [],\n      search: '',\n      streaming: false,\n    }\n  },\n\n  computed: {\n    filteredItems() {\n      const { search, items } = this\n      if (!search)\n        return items\n      const lowerCaseSearch = search.toLowerCase()\n      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))\n    },\n  },\n\n  unmounted() {\n    this.stopStream()\n  },\n\n  methods: {\n    changeMessage(message) {\n      Object.assign(message, generateMessage())\n    },\n\n    addMessage() {\n      for (let i = 0; i < 10; i++) {\n        this.items.push({\n          id: id++,\n          ...messages[id % 10000],\n        })\n      }\n      this.scrollToBottom()\n\n      if (this.streaming) {\n        requestAnimationFrame(this.addMessage)\n      }\n    },\n\n    scrollToBottom() {\n      this.$refs.scroller.scrollToBottom()\n    },\n\n    startStream() {\n      if (this.streaming)\n        return\n      this.streaming = true\n      this.addMessage()\n    },\n\n    stopStream() {\n      this.streaming = false\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"chat-demo\">\n    <div class=\"toolbar\">\n      <button\n        v-if=\"!streaming\"\n        @click=\"startStream()\"\n      >\n        Start stream\n      </button>\n      <button\n        v-else\n        @click=\"stopStream()\"\n      >\n        Stop stream\n      </button>\n\n      <input\n        v-model=\"search\"\n        placeholder=\"Filter...\"\n      >\n    </div>\n\n    <DynamicScroller\n      ref=\"scroller\"\n      :items=\"filteredItems\"\n      :min-item-size=\"54\"\n      class=\"scroller\"\n    >\n      <template #before>\n        <div class=\"notice\">\n          The message heights are unknown.\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[\n            item.message,\n          ]\"\n          :data-index=\"index\"\n          :data-active=\"active\"\n          :title=\"`Click to change message ${index}`\"\n          class=\"message\"\n          @click=\"changeMessage(item)\"\n        >\n          <div class=\"avatar\">\n            <img\n              :key=\"item.avatar\"\n              :src=\"item.avatar\"\n              alt=\"avatar\"\n              class=\"image\"\n            >\n          </div>\n          <div class=\"text\">\n            {{ item.message }}\n          </div>\n          <div class=\"index\">\n            <span>{{ item.id }} (id)</span>\n            <span>{{ index }} (index)</span>\n          </div>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </div>\n</template>\n\n<style scoped>\n.chat-demo {\n  overflow: hidden;\n  flex: auto 1 1;\n  display: flex;\n  flex-direction: column;\n}\n\n.scroller {\n  flex: auto 1 1;\n}\n\n.notice {\n  padding: 24px;\n  font-size: 20px;\n  color: #999;\n}\n\n.message {\n  display: flex;\n  min-height: 32px;\n  padding: 12px;\n  box-sizing: border-box;\n}\n\n.avatar {\n  flex: auto 0 0;\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n  margin-right: 12px;\n}\n\n.avatar .image {\n  max-width: 100%;\n  max-height: 100%;\n  border-radius: 50%;\n}\n\n.index,\n.text {\n  flex: 1;\n}\n\n.text {\n  max-width: 400px;\n}\n\n.index {\n  opacity: .5;\n}\n\n.index span {\n  display: inline-block;\n  width: 160px;\n  text-align: right;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/DynamicScrollerDemo.vue",
    "content": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push({\n    id: i,\n    ...generateMessage(),\n  })\n}\n\nexport default {\n  data() {\n    return {\n      items,\n      search: '',\n      updateParts: { viewStartIdx: 0, viewEndIdx: 0, visibleStartIdx: 0, visibleEndIdx: 0 },\n    }\n  },\n\n  computed: {\n    filteredItems() {\n      const { search, items } = this\n      if (!search)\n        return items\n      const lowerCaseSearch = search.toLowerCase()\n      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))\n    },\n  },\n\n  methods: {\n    changeMessage(message) {\n      Object.assign(message, generateMessage())\n    },\n\n    onUpdate(viewStartIndex, viewEndIndex, visibleStartIndex, visibleEndIndex) {\n      this.updateParts.viewStartIdx = viewStartIndex\n      this.updateParts.viewEndIdx = viewEndIndex\n      this.updateParts.visibleStartIdx = visibleStartIndex\n      this.updateParts.visibleEndIdx = visibleEndIndex\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"dynamic-scroller-demo\">\n    <div class=\"toolbar\">\n      <input\n        v-model=\"search\"\n        placeholder=\"Filter...\"\n      >\n      <span>({{ updateParts.viewStartIdx }} - [{{ updateParts.visibleStartIdx }} - {{ updateParts.visibleEndIdx }}] - {{ updateParts.viewEndIdx }})</span>\n    </div>\n\n    <DynamicScroller\n      :items=\"filteredItems\"\n      :min-item-size=\"54\"\n      :emit-update=\"true\"\n      class=\"scroller\"\n      @update=\"onUpdate\"\n    >\n      <template #before>\n        <div class=\"notice\">\n          The message heights are unknown.\n        </div>\n      </template>\n      <template #after>\n        <div class=\"notice\">\n          You have reached the end.\n        </div>\n      </template>\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[\n            item.message,\n          ]\"\n          :data-index=\"index\"\n          :data-active=\"active\"\n          :title=\"`Click to change message ${index}`\"\n          class=\"message\"\n          @click=\"changeMessage(item)\"\n        >\n          <div class=\"avatar\">\n            <img\n              :key=\"item.avatar\"\n              :src=\"item.avatar\"\n              alt=\"avatar\"\n              class=\"image\"\n            >\n          </div>\n          <div class=\"text\">\n            {{ item.message }}\n          </div>\n          <div class=\"index\">\n            <span>{{ item.id }} (id)</span>\n            <span>{{ index }} (index)</span>\n          </div>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </div>\n</template>\n\n<style scoped>\n.dynamic-scroller-demo {\n  height: 100%;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.scroller {\n  flex: auto 1 1;\n}\n\n.scroller {\n  border: solid 1px #42b983;\n}\n\n.toolbar {\n  flex: auto 0 0;\n  text-align: center;\n}\n\n.toolbar > *:not(:last-child) {\n  margin-right: 24px;\n}\n\n.notice {\n  padding: 24px;\n  font-size: 20px;\n  color: #999;\n}\n\n.message {\n  display: flex;\n  min-height: 32px;\n  padding: 12px;\n  box-sizing: border-box;\n}\n\n.avatar {\n  flex: auto 0 0;\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n  margin-right: 12px;\n}\n\n.avatar .image {\n  max-width: 100%;\n  max-height: 100%;\n  border-radius: 50%;\n}\n\n.index,\n.text {\n  flex: 1;\n}\n\n.text {\n  max-width: 400px;\n}\n\n.index {\n  opacity: .5;\n}\n\n.index span {\n  display: inline-block;\n  width: 160px;\n  text-align: right;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/GridDemo.vue",
    "content": "<script>\nimport { getData } from '../data'\n\nexport default {\n  data() {\n    return {\n      list: [],\n      gridItems: 6,\n      scrollTo: 500,\n    }\n  },\n\n  mounted() {\n    this.list = getData(5000)\n  },\n}\n</script>\n\n<template>\n  <div class=\"wrapper\">\n    <div class=\"toolbar\">\n      <label>\n        Grid items per row\n        <input\n          v-model.number=\"gridItems\"\n          type=\"number\"\n          min=\"2\"\n          max=\"20\"\n        >\n      </label>\n      <input\n        v-model.number=\"gridItems\"\n        type=\"range\"\n        min=\"2\"\n        max=\"20\"\n      >\n      <span>\n        <button @mousedown=\"$refs.scroller.scrollToItem(scrollTo)\">Scroll To: </button>\n        <input\n          v-model.number=\"scrollTo\"\n          type=\"number\"\n          min=\"0\"\n          :max=\"list.length - 1\"\n        >\n      </span>\n    </div>\n\n    <RecycleScroller\n      ref=\"scroller\"\n      class=\"scroller\"\n      :items=\"list\"\n      :item-size=\"128\"\n      :grid-items=\"gridItems\"\n      :item-secondary-size=\"100\"\n    >\n      <template #default=\"{ item, index }\">\n        <div class=\"item\">\n          <img\n            :key=\"item.id\"\n            :src=\"item.value.avatar\"\n          >\n          <div class=\"index\">\n            {{ index }}\n          </div>\n        </div>\n      </template>\n    </RecycleScroller>\n  </div>\n</template>\n\n<style scoped>\n.wrapper,\n.scroller {\n  height: 100%;\n}\n\n.wrapper {\n  display: flex;\n  flex-direction: column;\n}\n\n.toolbar {\n  flex: none;\n}\n\n.scroller {\n  flex: 1;\n}\n\n.scroller :deep(.hover) img {\n  opacity: 0.5;\n}\n\n.item {\n  position: relative;\n  height: 100%;\n}\n\n.index {\n  position: absolute;\n  top: 2px;\n  left: 2px;\n  padding: 4px;\n  border-radius: 4px;\n  background-color: rgba(255, 255, 255, 0.85);\n  color: black;\n}\n\nimg {\n  width: 100%;\n  height: 100%;\n  background: #eee;\n  object-fit: cover;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/Home.vue",
    "content": "<template>\n  <div class=\"home\">\n    <h1>Virtual scrolling solutions</h1>\n\n    <section>\n      <router-link\n        :to=\"{ name: 'recycle' }\"\n        class=\"route\"\n      >\n        Recycle Scroller\n      </router-link>\n      <div class=\"description\">\n        The Recycle Scroller is a component that only renders the visible item in your list. It also re-use components and dom elements to be the most efficient and performant possible.\n      </div>\n    </section>\n\n    <section>\n      <router-link\n        :to=\"{ name: 'dynamic' }\"\n        class=\"route\"\n      >\n        Dynamic Scroller\n      </router-link>\n      <div class=\"description\">\n        This component is using Recycle Scroller under-the-hood and adds a dynamic height management feature on top of it. The main use case for this is <b>not knowing the height of the items</b> in advance: the Dynamic Scroller will automatically \"discover\" it when it renders new item as the user scrolls.<br>\n        The items need to be wrapped in a special Dynamic Scroller Item component.\n      </div>\n    </section>\n\n    <section>\n      <a\n        href=\"https://github.com/Akryum/vue-virtual-scroller\"\n        class=\"route\"\n      >Documentation</a>\n      <div class=\"description\">\n        Read the full documentation on the repository.\n      </div>\n    </section>\n  </div>\n</template>\n\n<style scoped>\n.home {\n  margin: 24px;\n}\n\nsection {\n  margin-top: 24px;\n}\n\nsection .route {\n  font-size: 24px;\n  color: #42b983;\n  display: block;\n  margin-bottom: 6px;\n}\n\nsection .description {\n  max-width: 500px;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/HorizontalDemo.vue",
    "content": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push({\n    id: i,\n    ...generateMessage(),\n  })\n}\n\nexport default {\n  data() {\n    return {\n      items,\n      search: '',\n      dismissInfo: false,\n    }\n  },\n\n  computed: {\n    filteredItems() {\n      const { search, items } = this\n      if (!search)\n        return items\n      const lowerCaseSearch = search.toLowerCase()\n      return items.filter(i => i.message.toLowerCase().includes(lowerCaseSearch))\n    },\n  },\n\n  methods: {\n    changeMessage(message) {\n      Object.assign(message, generateMessage())\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"dynamic-scroller-demo\">\n    <div class=\"toolbar\">\n      <input\n        v-model=\"search\"\n        placeholder=\"Filter...\"\n      >\n    </div>\n\n    <DynamicScroller\n      :items=\"filteredItems\"\n      :min-item-size=\"54\"\n      direction=\"horizontal\"\n      class=\"scroller\"\n    >\n      <template #before>\n        <div\n          v-if=\"!dismissInfo\"\n          class=\"notice\"\n        >\n          <div>The message widths are unknown.</div>\n          <div>Scroll to the left ➡️</div>\n          <div>\n            <button @click=\"dismissInfo = true\">\n              OK\n            </button>\n          </div>\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :size-dependencies=\"[\n            item.message,\n          ]\"\n          :data-index=\"index\"\n          :data-active=\"active\"\n          :title=\"`Click to change message ${index}`\"\n          :style=\"{\n            width: `${Math.max(130, Math.round(item.message.length / 20 * 20))}px`,\n          }\"\n          class=\"message\"\n          @click=\"changeMessage(item)\"\n        >\n          <div class=\"avatar\">\n            <img\n              :key=\"item.avatar\"\n              :src=\"item.avatar\"\n              alt=\"avatar\"\n              class=\"image\"\n            >\n          </div>\n          <div class=\"text\">\n            {{ item.message }}\n          </div>\n          <div class=\"index\">\n            <span>{{ item.id }} (id)</span>\n            <span>{{ index }} (index)</span>\n          </div>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </div>\n</template>\n\n<style scoped>\n.dynamic-scroller-demo {\n  height: 100%;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.scroller {\n  flex: auto 1 1;\n}\n\n.notice {\n  padding: 24px;\n  font-size: 20px;\n  color: #999;\n}\n\n.message {\n  display: flex;\n  flex-direction: column;\n  min-height: 32px;\n  padding: 12px;\n  box-sizing: border-box;\n}\n\n.avatar {\n  flex: auto 0 0;\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n  margin-bottom: 12px;\n}\n\n.avatar .image {\n  max-width: 100%;\n  max-height: 100%;\n  border-radius: 50%;\n}\n\n.index,\n.text {\n  flex: 1;\n}\n\n.text {\n  margin-bottom: 12px;\n}\n\n.index {\n  opacity: .5;\n}\n\n.index span {\n  display: block;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/Person.vue",
    "content": "<script>\nexport default {\n  props: ['item', 'index'],\n\n  methods: {\n    edit() {\n      // eslint-disable-next-line vue/no-mutating-props\n      this.item.value.name += '#'\n    },\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"tr person\"\n    @click=\"edit\"\n  >\n    <div class=\"td index\">\n      {{ index }}\n    </div>\n    <div class=\"td\">\n      <div class=\"info\">\n        <img\n          :key=\"item.value.avatar\"\n          class=\"avatar\"\n          :src=\"item.value.avatar\"\n        >\n        <span>{{ item.value.name }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.index {\n  color: rgba(0, 0, 0, 0.2);\n  width: 55px;\n  text-align: right;\n  flex: auto 0 0;\n}\n\n.person .td:first-child {\n  padding: 12px;\n}\n\n.person .info {\n  display: flex;\n  align-items: center;\n  height: 48px;\n}\n\n.avatar {\n  width: 50px;\n  height: 50px;\n  margin-right: 12px;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/RecycleScrollerDemo.vue",
    "content": "<script>\nimport { addItem, getData } from '../data'\n\nimport Person from './Person.vue'\n\nexport default {\n  components: {\n    Person,\n  },\n\n  data: () => ({\n    items: [],\n    count: 10000,\n    renderScroller: true,\n    showScroller: true,\n    scopedSlots: false,\n    buffer: 200,\n    poolSize: 2000,\n    enableLetters: true,\n    pageMode: false,\n    pageModeFullPage: true,\n    scrollTo: 100,\n    updateParts: { viewStartIdx: 0, viewEndIdx: 0, visibleStartIdx: 0, visibleEndIdx: 0 },\n    showMessageBeforeItems: true,\n  }),\n\n  computed: {\n    countInput: {\n      get() {\n        return this.count\n      },\n      set(val) {\n        if (val > 500000) {\n          val = 500000\n        }\n        else if (val < 0) {\n          val = 0\n        }\n        this.count = val\n      },\n    },\n\n    itemHeight() {\n      return this.enableLetters ? null : 50\n    },\n\n    list() {\n      return this.items.map(\n        item => ({ ...{\n          random: Math.random(),\n        }, ...item }),\n      )\n    },\n  },\n\n  watch: {\n    count() {\n      this.generateItems()\n    },\n    enableLetters() {\n      this.generateItems()\n    },\n  },\n\n  mounted() {\n    this.$nextTick(this.generateItems)\n    window.scroller = this.$refs.scroller\n  },\n\n  methods: {\n    generateItems() {\n      this._dirty = true\n      this.items = getData(this.count, this.enableLetters)\n    },\n\n    addItem() {\n      addItem(this.items)\n    },\n\n    onUpdate(viewStartIndex, viewEndIndex, visibleStartIndex, visibleEndIndex) {\n      this.updateParts.viewStartIdx = viewStartIndex\n      this.updateParts.viewEndIdx = viewEndIndex\n      this.updateParts.visibleStartIdx = visibleStartIndex\n      this.updateParts.visibleEndIdx = visibleEndIndex\n    },\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"recycle-scroller-demo\"\n    :class=\"{\n      'page-mode': pageMode,\n      'full-page': pageModeFullPage,\n    }\"\n  >\n    <div class=\"toolbar\">\n      <span>\n        <input\n          v-model=\"countInput\"\n          type=\"number\"\n          min=\"0\"\n          max=\"500000\"\n        > items\n        <button @click=\"addItem()\">+1</button>\n      </span>\n      <label>\n        <input\n          v-model=\"enableLetters\"\n          type=\"checkbox\"\n        > variable height\n      </label>\n      <label>\n        <input\n          v-model=\"pageMode\"\n          type=\"checkbox\"\n        > page mode\n      </label>\n      <label v-if=\"pageMode\">\n        <input\n          v-model=\"pageModeFullPage\"\n          type=\"checkbox\"\n        > full page\n      </label>\n      <span>\n        <input\n          v-model.number=\"buffer\"\n          type=\"number\"\n          min=\"1\"\n          max=\"500000\"\n        > buffer\n      </span>\n      <span>\n        <button @mousedown=\"$refs.scroller.scrollToItem(scrollTo)\">Scroll To: </button>\n        <input\n          v-model.number=\"scrollTo\"\n          type=\"number\"\n          min=\"0\"\n          :max=\"list.length - 1\"\n        >\n      </span>\n      <span>\n        <button @mousedown=\"renderScroller = !renderScroller\">Toggle render</button>\n        <button @mousedown=\"showScroller = !showScroller\">Toggle visibility</button>\n      </span>\n      <label>\n        <input\n          v-model=\"showMessageBeforeItems\"\n          type=\"checkbox\"\n        > show message before items\n      </label>\n      <span>({{ updateParts.viewStartIdx }} - [{{ updateParts.visibleStartIdx }} - {{ updateParts.visibleEndIdx }}] - {{ updateParts.viewEndIdx }})</span>\n    </div>\n\n    <div\n      v-if=\"renderScroller\"\n      v-show=\"showScroller\"\n      class=\"content\"\n    >\n      <div class=\"wrapper\">\n        <RecycleScroller\n          :key=\"pageModeFullPage\"\n          ref=\"scroller\"\n          class=\"scroller\"\n          :items=\"list\"\n          :item-size=\"itemHeight\"\n          :buffer=\"buffer\"\n          :page-mode=\"pageMode\"\n          key-field=\"id\"\n          size-field=\"height\"\n          :emit-update=\"true\"\n          @update=\"onUpdate\"\n        >\n          <template #default=\"props\">\n            <div\n              v-if=\"props.item.type === 'letter'\"\n              class=\"tr letter big\"\n              @click=\"props.item.height = (props.item.height === 200 ? 300 : 200)\"\n            >\n              <div class=\"td index\">\n                {{ props.index }}\n              </div>\n              <div class=\"td value\">\n                {{ props.item.value }} Scoped\n              </div>\n            </div>\n            <Person\n              v-if=\"props.item.type === 'person'\"\n              :item=\"props.item\"\n              :index=\"props.index\"\n            />\n          </template>\n        </RecycleScroller>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.recycle-scroller-demo:not(.page-mode) {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.recycle-scroller-demo.page-mode:not(.full-page) {\n  height: 100%;\n}\n\n.recycle-scroller-demo.page-mode {\n  flex: auto 0 0;\n}\n\n.recycle-scroller-demo.page-mode .toolbar {\n  border-bottom: solid 1px #e0edfa;\n}\n\n.content {\n  flex: 100% 1 1;\n  border: solid 1px #42b983;\n  position: relative;\n}\n\n.recycle-scroller-demo.page-mode:not(.full-page) .content {\n  overflow: auto;\n}\n\n.recycle-scroller-demo:not(.page-mode) .wrapper {\n  overflow: hidden;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n\n.scroller {\n  width: 100%;\n  height: 100%;\n}\n\n.notice {\n  padding: 24px;\n  font-size: 20px;\n  color: #999;\n}\n\n.letter {\n  text-transform: uppercase;\n  color: grey;\n  font-weight: bold;\n}\n\n.letter .td {\n  padding: 12px;\n}\n\n.letter.big {\n  font-weight: normal;\n  height: 200px;\n}\n\n.letter.big .value {\n  font-size: 120px;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/SimpleList.vue",
    "content": "<script>\nimport { generateMessage } from '../data'\n\nconst items = []\nfor (let i = 0; i < 10000; i++) {\n  items.push(generateMessage().message)\n}\n\nexport default {\n  data() {\n    return {\n      items,\n      search: '',\n      dynamic: true,\n    }\n  },\n\n  computed: {\n    filteredItems() {\n      const { search, items } = this\n      if (!search)\n        return items\n      const lowerCaseSearch = search.toLowerCase()\n      return items.filter(i => i.toLowerCase().includes(lowerCaseSearch))\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"dynamic-scroller-demo\">\n    <div class=\"toolbar\">\n      <input\n        v-model=\"search\"\n        placeholder=\"Filter...\"\n      >\n\n      <label>\n        <input\n          v-model=\"dynamic\"\n          type=\"checkbox\"\n        >\n        Dynamic scroller\n      </label>\n    </div>\n\n    <DynamicScroller\n      v-if=\"dynamic\"\n      :items=\"filteredItems\"\n      :min-item-size=\"54\"\n      class=\"scroller\"\n    >\n      <template #before-container>\n        <div class=\"notice\">\n          Array of simple strings (no objects).\n        </div>\n      </template>\n\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :index=\"index\"\n          :active=\"active\"\n          :data-index=\"index\"\n          :data-active=\"active\"\n          class=\"message\"\n        >\n          <div class=\"text\">\n            {{ item }}\n          </div>\n          <div class=\"index\">\n            <span>{{ index }} (index)</span>\n          </div>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n\n    <RecycleScroller\n      v-else\n      :items=\"filteredItems.map((o, i) => `${i}: ${o.substr(0, 42)}...`)\"\n      :item-size=\"54\"\n      class=\"scroller\"\n    >\n      <template #default=\"{ item, index }\">\n        <div class=\"message\">\n          <div class=\"text\">\n            {{ item }}\n          </div>\n          <div class=\"index\">\n            <span>{{ index }} (index)</span>\n          </div>\n        </div>\n      </template>\n    </RecycleScroller>\n  </div>\n</template>\n\n<style scoped>\n.dynamic-scroller-demo {\n  flex: auto 1 1;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.scroller {\n  flex: auto 1 1;\n}\n\n.notice {\n  padding: 24px;\n  font-size: 20px;\n  color: #999;\n}\n\n.message {\n  display: flex;\n  min-height: 32px;\n  padding: 12px;\n  box-sizing: border-box;\n}\n\n.index,\n.text {\n  flex: 1;\n}\n\n.text {\n  max-width: 400px;\n}\n\n.index span {\n  display: inline-block;\n  width: 160px;\n  text-align: right;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/components/TestChat.vue",
    "content": "<script>\nimport { faker } from '@faker-js/faker'\n\nexport default {\n  name: 'TestChat',\n\n  data() {\n    return {\n      items: [],\n    }\n  },\n\n  methods: {\n    addItems(count = 1) {\n      for (let i = 0; i < count; i++) {\n        this.items.push({\n          text: faker.lorem.lines(),\n          id: this.items.length + 1,\n        })\n      }\n      this.scrollToBottom()\n    },\n\n    scrollToBottom() {\n      this.$refs.scroller.scrollToBottom()\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"hello\">\n    <div>\n      <button @click=\"addItems()\">\n        Add item\n      </button>\n      <button @click=\"addItems(5)\">\n        Add 5 items\n      </button>\n      <button @click=\"addItems(10)\">\n        Add 10 items\n      </button>\n      <button @click=\"addItems(50)\">\n        Add 50 items\n      </button>\n    </div>\n\n    <DynamicScroller\n      ref=\"scroller\"\n      :items=\"items\"\n      :min-item-size=\"24\"\n      class=\"scroller\"\n      @resize=\"scrollToBottom()\"\n    >\n      <template #default=\"{ item, index, active }\">\n        <DynamicScrollerItem\n          :item=\"item\"\n          :active=\"active\"\n          :data-index=\"index\"\n        >\n          <div class=\"message\">\n            {{ item.text }}\n          </div>\n        </DynamicScrollerItem>\n      </template>\n    </DynamicScroller>\n  </div>\n</template>\n\n<style scoped>\n.hello {\n  flex: 0 1 1;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.scroller {\n  flex: auto 1 1;\n  border: 2px solid #ddd;\n}\n\nh1,\nh2 {\n  font-weight: normal;\n}\nul {\n  list-style-type: none;\n  padding: 0;\n}\nli {\n  display: inline-block;\n  margin: 0 10px;\n}\na {\n  color: #42b983;\n}\n\n.message {\n  padding: 10px 10px 9px;\n  border-bottom: solid 1px #eee;\n}\n</style>\n"
  },
  {
    "path": "packages/demo/src/data.js",
    "content": "import { faker } from '@faker-js/faker'\n\nlet uid = 0\n\nfunction generateItem() {\n  return {\n    name: faker.name.fullName(),\n    avatar: faker.internet.avatar(),\n  }\n}\n\nexport function getData(count, letters) {\n  const raw = {}\n\n  const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('')\n\n  for (const l of alphabet) {\n    raw[l] = []\n  }\n\n  for (let i = 0; i < count; i++) {\n    const item = generateItem()\n    const letter = item.name.charAt(0).toLowerCase()\n    raw[letter].push(item)\n  }\n\n  const list = []\n  let index = 1\n\n  for (const l of alphabet) {\n    raw[l] = raw[l].sort((a, b) => a.name < b.name ? -1 : 1)\n    if (letters) {\n      list.push({\n        id: uid++,\n        index: index++,\n        type: 'letter',\n        value: l,\n        height: 200,\n      })\n    }\n    for (const item of raw[l]) {\n      list.push({\n        id: uid++,\n        index: index++,\n        type: 'person',\n        value: item,\n        height: 50,\n      })\n    }\n  }\n\n  return list\n}\n\nexport function addItem(list) {\n  list.push({\n    id: uid++,\n    index: list.length + 1,\n    type: 'person',\n    value: generateItem(),\n    height: 50,\n  })\n}\n\nexport function generateMessage() {\n  return {\n    avatar: faker.internet.avatar(),\n    message: faker.lorem.text(),\n  }\n}\n"
  },
  {
    "path": "packages/demo/src/main.js",
    "content": "import { createApp } from 'vue'\n\nimport VirtualScroller from 'vue-virtual-scroller'\nimport App from './App.vue'\n\nimport router from './router'\nimport 'vue-virtual-scroller/dist/vue-virtual-scroller.css'\n\nconst app = createApp(App)\napp.use(router)\napp.use(VirtualScroller)\napp.mount('#app')\n"
  },
  {
    "path": "packages/demo/src/router.js",
    "content": "import { createRouter, createWebHistory } from 'vue-router'\n\nimport ChatDemo from './components/ChatDemo.vue'\nimport Dynamic from './components/DynamicScrollerDemo.vue'\nimport GridDemo from './components/GridDemo.vue'\nimport Home from './components/Home.vue'\nimport HorizontalDemo from './components/HorizontalDemo.vue'\nimport Recycle from './components/RecycleScrollerDemo.vue'\nimport SimpleList from './components/SimpleList.vue'\nimport TestChat from './components/TestChat.vue'\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [\n    { path: '/', name: 'home', component: Home },\n    { path: '/recycle', name: 'recycle', component: Recycle },\n    { path: '/dynamic', name: 'dynamic', component: Dynamic },\n    { path: '/test-chat', name: 'test-chat', component: TestChat },\n    { path: '/simple-list', name: 'simple-list', component: SimpleList },\n    { path: '/horizontal', name: 'horizontal', component: HorizontalDemo },\n    { path: '/chat', name: 'chat', component: ChatDemo },\n    { path: '/grid', name: 'grid', component: GridDemo },\n  ],\n})\n\nexport default router\n"
  },
  {
    "path": "packages/demo/vite.config.js",
    "content": "import vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [vue()],\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 guillaume.b.chau@gmail.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/vue-virtual-scroller/README.md",
    "content": "# vue-virtual-scroller\n\n[![npm](https://img.shields.io/npm/v/vue-virtual-scroller.svg) ![npm](https://img.shields.io/npm/dm/vue-virtual-scroller.svg)](https://www.npmjs.com/package/vue-virtual-scroller)\n[![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/)\n\n[Documentation](https://vue-virtual-scroller.netlify.app/)\n\nBlazing fast scrolling of any amount of data | [Live demo](https://vue-virtual-scroller-demo.netlify.app/) | [Video demo](https://www.youtube.com/watch?v=Uzq1KQV8f4k)\n\nFor Vue 2 support, see [here](https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller)\n\nThis package ships ESM only in the current Vue 3 line. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.\n\n[💚️ Become a Sponsor](https://github.com/sponsors/Akryum)\n\n## Sponsors\n\n<p align=\"center\">\n  <a href=\"https://guillaume-chau.info/sponsors/\" target=\"_blank\">\n    <img src='https://akryum.netlify.app/sponsors.svg' alt=\"sponsors\" />\n  </a>\n</p>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/package.json",
    "content": "{\n  \"name\": \"vue-virtual-scroller\",\n  \"type\": \"module\",\n  \"version\": \"2.0.0-beta.10\",\n  \"description\": \"Smooth scrolling for any amount of data\",\n  \"author\": {\n    \"name\": \"Guillaume Chau\",\n    \"email\": \"guillaume.b.chau@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/Akryum/vue-virtual-scroller#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Akryum/vue-virtual-scroller.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/Akryum/vue-virtual-scroller/issues\"\n  },\n  \"keywords\": [\n    \"vue\",\n    \"vuejs\",\n    \"plugin\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/vue-virtual-scroller.js\",\n      \"default\": \"./dist/vue-virtual-scroller.js\"\n    },\n    \"./dist/vue-virtual-scroller.css\": \"./dist/vue-virtual-scroller.css\",\n    \"./index.css\": \"./dist/vue-virtual-scroller.css\"\n  },\n  \"module\": \"dist/vue-virtual-scroller.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"skills\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"dev\": \"vite build --watch\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"prepublishOnly\": \"pnpm run lint && pnpm run build\",\n    \"lint\": \"cd ../../ && pnpm run lint\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^3.2.0\"\n  },\n  \"dependencies\": {\n    \"mitt\": \"^2.1.0\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^5.0.0\",\n    \"@vue/test-utils\": \"^2.4.6\",\n    \"jsdom\": \"^28.1.0\",\n    \"typescript\": \"^5.3.3\",\n    \"vite\": \"^6.0.0\",\n    \"vite-plugin-dts\": \"^4.0.0\",\n    \"vitest\": \"4.0.16\",\n    \"vue\": \"^3.2.41\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/SKILL.md",
    "content": "---\nname: vue-virtual-scroller\ndescription: Use this skill for Vue 3 virtual scrolling with vue-virtual-scroller, including RecycleScroller, DynamicScroller, DynamicScrollerItem, and the useRecycleScroller headless composable for fixed-size lists, unknown-size rows, grids, chat feeds, and horizontal layouts.\n---\n\n# Vue Virtual Scroller\n\nUse this skill when a task involves large Vue lists, DOM reuse, windowed rendering, or choosing between `RecycleScroller`, `DynamicScroller`, and headless virtualization with `useRecycleScroller`.\n\n## Quick choice\n\n| Surface | Use it when | Avoid it when |\n|---|---|---|\n| `RecycleScroller` | Item size is fixed or precomputed, or items expose a numeric size field for variable-size mode. | You need automatic measurement of unknown item sizes. |\n| `DynamicScroller` | Item sizes are not known ahead of time and should be discovered during rendering. | You can provide a stable fixed size and want the lightest path. |\n| `DynamicScrollerItem` | You are rendering children inside `DynamicScroller` and need size measurement updates. | You are not inside `DynamicScroller`. |\n| `useRecycleScroller` | You need the virtualization engine but want custom markup, styling, or rendering control. | The slot-based component APIs already fit the UI. |\n\n## Setup\n\n`vue-virtual-scroller` targets Vue 3 and ships ESM only. Use it with an ESM-aware toolchain such as Vite, Nuxt, Rollup, or webpack 5.\n\n```sh\npnpm add vue-virtual-scroller@next\n```\n\nAlways import the package CSS:\n\n```js\nimport 'vue-virtual-scroller/index.css'\n```\n\nInstall all bundled components:\n\n```js\nimport { createApp } from 'vue'\nimport VueVirtualScroller from 'vue-virtual-scroller'\n\nconst app = createApp(App)\napp.use(VueVirtualScroller)\n```\n\nOr register/import only what you need:\n\n```js\nimport { RecycleScroller } from 'vue-virtual-scroller'\n\napp.component('RecycleScroller', RecycleScroller)\n```\n\n## Workflow\n\n1. Decide whether sizes are known.\n2. If sizes are fixed or already available on each item, start with `RecycleScroller`.\n3. If sizes are unknown and discovered after render, use `DynamicScroller` with `DynamicScrollerItem`.\n4. If the component slot structure is too limiting, switch to `useRecycleScroller`.\n5. Set explicit scroll-container sizing and item sizing before debugging performance.\n\n## Sizing rules\n\n- The scroller element itself must have a real scrollable size such as a fixed `height` or `width` plus overflow.\n- In `RecycleScroller`, all items should have the same size unless you intentionally use variable-size mode with `itemSize: null` and a numeric item field such as `size`.\n- In `DynamicScroller`, `minItemSize` is required for initial layout.\n- Horizontal lists use the same primitives, but sizing constraints apply on width instead of height.\n- Grid mode is only supported with `RecycleScroller` and fixed item sizing.\n\n## Practical guidance\n\n### Fixed-size vs unknown-size\n\n- Prefer `RecycleScroller` for tables, simple rows, and card lists where row height or card extent is stable.\n- Use `RecycleScroller` variable-size mode only when the item already knows its size and can expose it through `sizeField`.\n- Prefer `DynamicScroller` when the DOM must measure content, such as chat messages or cards whose rendered content changes height.\n\n### Rendering pitfalls\n\n- Reused views mean child components must react correctly when `item` changes; do not assume a fresh component instance per row.\n- Functional components inside `RecycleScroller` are discouraged because reuse makes them slower, not faster.\n- Do not add unnecessary `key` values to the immediate list content, but do key nested images to avoid load glitches.\n- Use the provided `hover` class for hover styling instead of relying on `:hover` against recycled DOM nodes.\n\n### Performance guardrails\n\n- Variable-size mode in `RecycleScroller` can be expensive on very large lists.\n- `watchData` on `DynamicScrollerItem` is documented as not recommended because deep watching can hurt performance.\n- `emitUpdate` on `RecycleScroller` and `emitResize` on `DynamicScrollerItem` add extra work; keep them off unless the UI needs those events.\n- Browsers still impose large-element size limits, so extremely large lists can hit practical limits around hundreds of thousands of items.\n\n### Common patterns\n\n- Chat feeds and append-heavy timelines map well to `DynamicScroller` plus `DynamicScrollerItem`.\n- Multi-column card galleries map to `RecycleScroller` grid mode with `gridItems` and `itemSecondarySize`.\n- Horizontal virtualized cards map to `DynamicScroller` with `direction=\"horizontal\"` when widths are content-driven.\n- Design-system integrations or nonstandard DOM trees map to `useRecycleScroller`.\n\n## Scope limits\n\nThis skill intentionally focuses on the documented public surfaces:\n\n- setup and installation\n- `RecycleScroller`\n- `DynamicScroller`\n- `DynamicScrollerItem`\n- `useRecycleScroller`\n\nDo not infer undocumented behavior for these exported surfaces without updating docs first:\n\n- `useDynamicScroller`\n- `useDynamicScrollerItem`\n- `useIdState`\n- plugin install options beyond the documented setup path\n\n## References\n\n| Topic | Description | Reference |\n|---|---|---|\n| Installation and setup | Vue 3 setup, ESM-only constraint, CSS import, and component registration paths. | [references/installation-and-setup.md](./references/installation-and-setup.md) |\n| RecycleScroller | Fixed-size lists, variable-size mode with explicit size fields, grid mode, page mode, and core events. | [references/recycle-scroller.md](./references/recycle-scroller.md) |\n| DynamicScroller | Unknown-size rendering strategy and when to move off the fixed-size path. | [references/dynamic-scroller.md](./references/dynamic-scroller.md) |\n| DynamicScrollerItem | Size measurement wrapper behavior, dependencies, and resize events. | [references/dynamic-scroller-item.md](./references/dynamic-scroller-item.md) |\n| useRecycleScroller | Headless virtualization API for custom DOM structures. | [references/use-recycle-scroller.md](./references/use-recycle-scroller.md) |\n| Reference index | Overview of all shipped references. | [references/index.md](./references/index.md) |\n\n## Further reading\n\n- [references/installation-and-setup.md](./references/installation-and-setup.md)\n- [references/recycle-scroller.md](./references/recycle-scroller.md)\n- [references/dynamic-scroller.md](./references/dynamic-scroller.md)\n- [references/dynamic-scroller-item.md](./references/dynamic-scroller-item.md)\n- [references/use-recycle-scroller.md](./references/use-recycle-scroller.md)\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller-item.md",
    "content": "# DynamicScrollerItem\n\nScope: the per-item measurement wrapper used inside `DynamicScroller`.\n\n## Provenance\n\nGenerated from the package's public dynamic-item documentation and shipped demo patterns at skill generation time.\n\n## When to use\n\n- You are rendering an item inside `DynamicScroller`.\n- The rendered content can change size after first render.\n- You need resize events for item-level measurement updates.\n\n## Required inputs\n\n- `item`\n- `active`\n\nRecommended for real updates:\n\n- `sizeDependencies`\n\n## Core props/options\n\n- `item`\n- `active`\n- `sizeDependencies`\n- `watchData`\n- `tag`\n- `emitResize`\n\nDocumented guidance:\n\n- Prefer `sizeDependencies` over `watchData`.\n- `watchData` deeply watches the item and is not recommended for performance-sensitive lists.\n\n## Events/returns\n\nDocumented event:\n\n- `resize` when `emitResize` is `true`\n\n## Pitfalls\n\n- Omitting `active` breaks the documented optimization path for avoiding unnecessary recomputation.\n- Reaching for `watchData` first is usually the wrong tradeoff; use targeted dependencies instead.\n- `emitResize` increases work and should be enabled only when the parent UI needs to react.\n\n## Example patterns\n\nTrack text-driven height changes:\n\n```vue\n<DynamicScrollerItem\n  :item=\"item\"\n  :active=\"active\"\n  :size-dependencies=\"[item.message]\"\n>\n  <p>{{ item.message }}</p>\n</DynamicScrollerItem>\n```\n\nCustom tag:\n\n```vue\n<DynamicScrollerItem\n  :item=\"item\"\n  :active=\"active\"\n  tag=\"article\"\n>\n  {{ item.title }}\n</DynamicScrollerItem>\n```\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/dynamic-scroller.md",
    "content": "# DynamicScroller\n\nScope: the component path for unknown-size items that must be measured as they render.\n\n## Provenance\n\nGenerated from the package's public dynamic-sizing documentation and shipped demo patterns at skill generation time.\n\n## When to use\n\n- Item heights or widths are not known before render.\n- Message-like content can grow or change after filtering or appending.\n- You need automatic size discovery instead of a precomputed size field.\n\n## Required inputs\n\n- `items`\n- `minItemSize` for the initial render path\n- A `DynamicScrollerItem` around each rendered item\n- Scroll-container sizing in CSS\n\n## Core props/options\n\n`DynamicScroller` extends the documented `RecycleScroller` props.\n\nKey documented guidance:\n\n- `minItemSize` is required.\n- It is not recommended to change `sizeField`, because size management is handled internally.\n- You do not need a `size` field on each item.\n\nImportant inherited props often used in practice:\n\n- `direction`\n- `buffer`\n- `pageMode`\n- `prerender`\n\n## Events/returns\n\n`DynamicScroller` extends the documented `RecycleScroller` events, slot props, and other slots.\n\nThe default slot still exposes:\n\n- `item`\n- `index`\n- `active`\n\n## Pitfalls\n\n- `DynamicScroller` does not detect size changes by itself; dynamic inputs must be forwarded to `DynamicScrollerItem` through `sizeDependencies`.\n- Missing `minItemSize` degrades the initial layout.\n- This path is heavier than fixed-size virtualization, so prefer `RecycleScroller` when item size is already known.\n\n## Example patterns\n\nUnknown-height rows:\n\n```vue\n<DynamicScroller\n  :items=\"items\"\n  :min-item-size=\"54\"\n>\n  <template #default=\"{ item, index, active }\">\n    <DynamicScrollerItem\n      :item=\"item\"\n      :active=\"active\"\n      :size-dependencies=\"[item.message]\"\n      :data-index=\"index\"\n    >\n      {{ item.message }}\n    </DynamicScrollerItem>\n  </template>\n</DynamicScroller>\n```\n\nAppend-heavy chat feed:\n\n```vue\n<DynamicScroller\n  ref=\"scroller\"\n  :items=\"rows\"\n  :min-item-size=\"48\"\n  @resize=\"scroller?.scrollToBottom()\"\n>\n  <template #default=\"{ item, active }\">\n    <DynamicScrollerItem\n      :item=\"item\"\n      :active=\"active\"\n      :size-dependencies=\"[item.text]\"\n    >\n      {{ item.text }}\n    </DynamicScrollerItem>\n  </template>\n</DynamicScroller>\n```\n\nHorizontal dynamic cards:\n\n```vue\n<DynamicScroller\n  :items=\"filteredRows\"\n  :min-item-size=\"180\"\n  direction=\"horizontal\"\n>\n  <template #default=\"{ item, active }\">\n    <DynamicScrollerItem\n      :item=\"item\"\n      :active=\"active\"\n      :size-dependencies=\"[item.message]\"\n    >\n      {{ item.message }}\n    </DynamicScrollerItem>\n  </template>\n</DynamicScroller>\n```\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/index.md",
    "content": "# Vue Virtual Scroller References\n\nFocused reference map for the documented public surfaces covered by this skill.\n\n| Topic | Description | Reference |\n|---|---|---|\n| Installation and setup | Package install, ESM-only note, CSS import, and component registration options. | [installation-and-setup.md](./installation-and-setup.md) |\n| RecycleScroller | Core component for fixed-size and pre-sized virtual lists, including grid and page mode. | [recycle-scroller.md](./recycle-scroller.md) |\n| DynamicScroller | Wrapper over `RecycleScroller` for unknown-size items measured during rendering. | [dynamic-scroller.md](./dynamic-scroller.md) |\n| DynamicScrollerItem | Required child wrapper for dynamic measurement in `DynamicScroller`. | [dynamic-scroller-item.md](./dynamic-scroller-item.md) |\n| useRecycleScroller | Headless composable for custom markup and rendering control. | [use-recycle-scroller.md](./use-recycle-scroller.md) |\n\n## Coverage notes\n\n- `IdState` is documented in the guide, but the package currently exports `useIdState`, so it is intentionally not shipped as a reference here until docs and exports are reconciled.\n- `useDynamicScroller` and `useDynamicScrollerItem` are exported but do not currently have dedicated guide pages, so they are omitted from this initial skill.\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/installation-and-setup.md",
    "content": "# Installation And Setup\n\nScope: install the package correctly in a Vue 3 app and avoid the common setup mistakes that make examples appear broken.\n\n## Provenance\n\nGenerated from the package's public setup documentation at skill generation time.\n\n## When to use\n\n- You are adding `vue-virtual-scroller` to a Vue 3 application.\n- You need to decide between global plugin install and direct component import.\n- A demo or example is not rendering because the package CSS or ESM requirement was missed.\n\n## Required inputs\n\n- Vue 3 application context.\n- ESM-aware build tool such as Vite, Nuxt, Rollup, or webpack 5.\n- The package stylesheet import: `vue-virtual-scroller/index.css`.\n\n## Core props/options\n\n- Global plugin install:\n  - `app.use(VueVirtualScroller)`\n- Direct component import:\n  - import and register `RecycleScroller`, `DynamicScroller`, or `DynamicScrollerItem` explicitly\n\nThe current guide documents the setup path, but does not fully document plugin option behavior. Keep setup guidance to the documented install patterns unless the docs are expanded first.\n\n## Events/returns\n\n- None at setup time.\n\n## Pitfalls\n\n- The package is ESM only in the current Vue 3 line.\n- Missing the CSS import will make layout and virtualization behavior appear incorrect.\n- Vue 2 references still exist in historical material, but they are out of scope for this package line.\n\n## Example patterns\n\nGlobal registration:\n\n```js\nimport { createApp } from 'vue'\nimport VueVirtualScroller from 'vue-virtual-scroller'\nimport 'vue-virtual-scroller/index.css'\n\nconst app = createApp(App)\napp.use(VueVirtualScroller)\n```\n\nDirect import:\n\n```js\nimport { RecycleScroller } from 'vue-virtual-scroller'\nimport 'vue-virtual-scroller/index.css'\n\napp.component('RecycleScroller', RecycleScroller)\n```\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/recycle-scroller.md",
    "content": "# RecycleScroller\n\nScope: the main component for virtualizing fixed-size lists, pre-sized variable lists, grids, and page-level scrolling.\n\n## Provenance\n\nGenerated from the package's public component documentation and shipped demo patterns at skill generation time.\n\n## When to use\n\n- Item height or width is fixed.\n- Each item already exposes a numeric size field and you want variable-size mode without DOM measurement.\n- You need grid rendering with fixed item dimensions.\n- You want page mode or SSR prerender support through the component API.\n\n## Required inputs\n\n- `items`\n- A stable scroller size in CSS.\n- `itemSize` for fixed-size mode, or `itemSize: null` plus a numeric size field for variable-size mode.\n- `keyField` when object items do not use `id`.\n\n## Core props/options\n\nCommon props:\n\n- `items`\n- `direction`\n- `itemSize`\n- `keyField`\n- `buffer`\n- `pageMode`\n- `prerender`\n\nVariable-size mode:\n\n- `itemSize: null`\n- `sizeField`\n- `minItemSize` for unknown item sizes before they are fully known\n\nGrid mode:\n\n- `gridItems`\n- `itemSecondarySize`\n\nOther documented props:\n\n- `typeField`\n- `emitUpdate`\n- `updateInterval`\n- `listClass`\n- `itemClass`\n- `listTag`\n- `itemTag`\n\n## Events/returns\n\nDocumented events:\n\n- `resize`\n- `visible`\n- `hidden`\n- `update(startIndex, endIndex, visibleStartIndex, visibleEndIndex)` when `emitUpdate` is enabled\n- `scroll-start`\n- `scroll-end`\n\nDefault slot props:\n\n- `item`\n- `index`\n- `active`\n\nNamed slots:\n\n- `before`\n- `empty`\n- `after`\n\n## Pitfalls\n\n- The scroller element and item elements must be sized correctly with CSS.\n- Do not use functional components in recycled views when performance matters.\n- Child components must respond to `item` changing because views are reused.\n- Nested images should still receive keys to avoid load glitches.\n- Use the `hover` class rather than raw `:hover` selectors on recycled nodes.\n- Browser element-size limits make extremely large lists impractical.\n- Variable-size mode can become expensive with many items.\n\n## Example patterns\n\nFixed-size rows:\n\n```vue\n<RecycleScroller\n  v-slot=\"{ item }\"\n  :items=\"list\"\n  :item-size=\"32\"\n  key-field=\"id\"\n>\n  <div class=\"row\">\n    {{ item.name }}\n  </div>\n</RecycleScroller>\n```\n\nGrid layout:\n\n```vue\n<RecycleScroller\n  :items=\"cards\"\n  :item-size=\"166\"\n  :grid-items=\"5\"\n  :item-secondary-size=\"176\"\n>\n  <template #default=\"{ item }\">\n    <article>{{ item.name }}</article>\n  </template>\n</RecycleScroller>\n```\n\nPage mode:\n\n```vue\n<RecycleScroller\n  :items=\"items\"\n  :item-size=\"42\"\n  page-mode\n/>\n```\n"
  },
  {
    "path": "packages/vue-virtual-scroller/skills/vue-virtual-scroller/references/use-recycle-scroller.md",
    "content": "# useRecycleScroller\n\nScope: the headless virtualization composable for building custom scroll UIs without the bundled component markup.\n\n## Provenance\n\nGenerated from the package's public headless virtualization documentation at skill generation time.\n\n## When to use\n\n- You need a custom DOM structure that does not fit the component slot API.\n- You want to integrate virtualization into an existing design-system component.\n- You want direct control over pooled views, scroll handling, and item placement.\n\n## Required inputs\n\n- A scroller element ref.\n- An options object containing the same core settings used by `RecycleScroller`:\n  - `items`\n  - `keyField`\n  - `direction`\n  - `itemSize`\n  - `minItemSize`\n  - `sizeField`\n  - `typeField`\n  - `buffer`\n  - `pageMode`\n  - `prerender`\n  - `emitUpdate`\n  - `updateInterval`\n\nOptional grid inputs:\n\n- `gridItems`\n- `itemSecondarySize`\n\n## Core props/options\n\nFixed-size path:\n\n- set `itemSize` to a number\n\nVariable-size path:\n\n- set `itemSize` to `null`\n- provide a numeric field on each item\n- set `sizeField` if that field is not `size`\n\nThe composable manages virtualization state, but markup, CSS, and event wiring stay in user land.\n\n## Events/returns\n\nDocumented returns used most often:\n\n- `pool`\n- `totalSize`\n- `handleScroll`\n- `scrollToItem(index)`\n- `scrollToPosition(px)`\n- `getScroll()`\n- `updateVisibleItems(itemsChanged, checkPositionDiff?)`\n\n## Pitfalls\n\n- You must provide your own scrollable sizing styles.\n- Without a stable key field, object-item reuse becomes unreliable.\n- This composable does not provide measurement for unknown-size items; move to `DynamicScroller` when content size must be discovered from the DOM.\n\n## Example patterns\n\nMinimal fixed-size setup:\n\n```vue\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useRecycleScroller } from 'vue-virtual-scroller'\n\nconst items = ref(Array.from({ length: 10000 }, (_, i) => ({\n  id: i + 1,\n  name: `User ${i + 1}`,\n})))\n\nconst scrollerEl = ref<HTMLElement>()\n\nconst options = computed(() => ({\n  items: items.value,\n  keyField: 'id',\n  direction: 'vertical' as const,\n  itemSize: 40,\n  minItemSize: null,\n  sizeField: 'size',\n  typeField: 'type',\n  buffer: 200,\n  pageMode: false,\n  prerender: 0,\n  emitUpdate: false,\n  updateInterval: 0,\n}))\n\nconst { pool, totalSize, handleScroll } = useRecycleScroller(options, scrollerEl)\n</script>\n```\n\nVariable-size setup with explicit item field:\n\n```ts\nconst options = computed(() => ({\n  items: items.value,\n  keyField: 'id',\n  direction: 'vertical' as const,\n  itemSize: null,\n  minItemSize: 40,\n  sizeField: 'size',\n  typeField: 'type',\n  buffer: 200,\n  pageMode: false,\n  prerender: 0,\n  emitUpdate: false,\n  updateInterval: 0,\n}))\n```\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScroller.spec.ts",
    "content": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { defineComponent, h } from 'vue'\nimport DynamicScroller from './DynamicScroller.vue'\n\nconst scrollerScrollToItem = vi.fn()\n\nconst RecycleScrollerStub = defineComponent({\n  name: 'RecycleScroller',\n  props: {\n    items: {\n      type: Array,\n      default: () => [],\n    },\n    minItemSize: {\n      type: [Number, String],\n      required: true,\n    },\n    direction: {\n      type: String,\n      default: 'vertical',\n    },\n    keyField: {\n      type: String,\n      default: 'id',\n    },\n    listTag: {\n      type: String,\n      default: 'div',\n    },\n    itemTag: {\n      type: String,\n      default: 'div',\n    },\n  },\n  emits: ['resize', 'visible'],\n  setup(props, { slots, emit, expose }) {\n    const el = document.createElement('div')\n    expose({\n      el,\n      scrollToItem: scrollerScrollToItem,\n    })\n\n    return () => h('div', { class: 'recycle-scroller-stub' }, [\n      slots.before?.(),\n      ...(props.items as unknown[]).map((item, index) => slots.default?.({\n        item,\n        index,\n        active: index === 0,\n      })),\n      ...(props.items as unknown[]).length === 0\n        ? slots.empty?.() ?? []\n        : [],\n      slots.after?.(),\n      h('button', {\n        class: 'emit-resize',\n        type: 'button',\n        onClick: () => emit('resize'),\n      }),\n      h('button', {\n        class: 'emit-visible',\n        type: 'button',\n        onClick: () => emit('visible'),\n      }),\n    ])\n  },\n})\n\nfunction mountDynamicScroller(props: any, slots?: any) {\n  return mount(DynamicScroller, {\n    props,\n    slots,\n    global: {\n      stubs: {\n        RecycleScroller: RecycleScrollerStub,\n      },\n    },\n  })\n}\n\ndescribe('dynamicScroller', () => {\n  beforeEach(() => {\n    scrollerScrollToItem.mockReset()\n  })\n\n  it('forwards slot bindings from itemWithSize', () => {\n    const items = [\n      { id: 'a', label: 'Alpha' },\n      { id: 'b', label: 'Beta' },\n    ]\n\n    const wrapper = mountDynamicScroller(\n      {\n        items,\n        minItemSize: 20,\n      },\n      {\n        default: ({ item, index, active, itemWithSize }: any) => h('div', { class: 'row' }, `${item.label}|${index}|${active ? 'active' : 'inactive'}|${itemWithSize.id}`),\n        before: () => h('div', { class: 'before-slot' }, 'before'),\n        after: () => h('div', { class: 'after-slot' }, 'after'),\n        empty: () => h('div', { class: 'empty-slot' }, 'empty'),\n      },\n    )\n\n    const rows = wrapper.findAll('.row')\n    expect(rows).toHaveLength(2)\n    expect(rows[0].text()).toBe('Alpha|0|active|a')\n    expect(rows[1].text()).toBe('Beta|1|inactive|b')\n    expect(wrapper.find('.before-slot').exists()).toBe(true)\n    expect(wrapper.find('.after-slot').exists()).toBe(true)\n    expect(wrapper.find('.empty-slot').exists()).toBe(false)\n  })\n\n  it('renders empty slot only when items is empty', () => {\n    const wrapper = mountDynamicScroller(\n      {\n        items: [],\n        minItemSize: 20,\n      },\n      {\n        empty: () => h('div', { class: 'empty-slot' }, 'empty'),\n      },\n    )\n\n    expect(wrapper.find('.empty-slot').exists()).toBe(true)\n  })\n\n  it('passes props to RecycleScroller and re-emits resize and visible', async () => {\n    const wrapper = mountDynamicScroller({\n      items: [{ id: 1 }],\n      minItemSize: 24,\n      direction: 'horizontal',\n      listTag: 'ul',\n      itemTag: 'li',\n    })\n\n    const scroller = wrapper.getComponent({ name: 'RecycleScroller' })\n    expect(scroller.props('minItemSize')).toBe(24)\n    expect(scroller.props('direction')).toBe('horizontal')\n    expect(scroller.props('listTag')).toBe('ul')\n    expect(scroller.props('itemTag')).toBe('li')\n\n    await scroller.get('.emit-resize').trigger('click')\n    await scroller.get('.emit-visible').trigger('click')\n\n    expect(wrapper.emitted('resize')).toHaveLength(1)\n    expect(wrapper.emitted('visible')).toHaveLength(1)\n  })\n\n  it('exposes scrollToItem and getItemSize', () => {\n    const items = [\n      { id: 'a', label: 'Alpha' },\n    ]\n    const wrapper = mountDynamicScroller({\n      items,\n      minItemSize: 20,\n    })\n    const vm = wrapper.vm as any\n\n    vm.scrollToItem(3)\n    expect(scrollerScrollToItem).toHaveBeenCalledWith(3)\n    expect(vm.getItemSize(items[0])).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScroller.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ItemWithSize, ScrollDirection } from '../types'\nimport { computed, ref } from 'vue'\nimport { useDynamicScroller } from '../composables/useDynamicScroller'\nimport RecycleScroller from './RecycleScroller.vue'\n\ndefineOptions({\n  inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<{\n  items: unknown[]\n  keyField?: string\n  direction?: ScrollDirection\n  listTag?: string\n  itemTag?: string\n  minItemSize: number | string\n}>(), {\n  keyField: 'id',\n  direction: 'vertical',\n  listTag: 'div',\n  itemTag: 'div',\n})\n\nconst emit = defineEmits<{\n  resize: []\n  visible: []\n}>()\n\n// Template refs\nconst scroller = ref<InstanceType<typeof RecycleScroller>>()\n\n// Derive the root DOM element from the scroller's exposed el ref\nconst scrollerEl = computed(() => scroller.value?.el)\n\nconst {\n  itemsWithSize,\n  forceUpdate,\n  scrollToItem,\n  getItemSize,\n  scrollToBottom,\n  onScrollerResize,\n  onScrollerVisible,\n} = useDynamicScroller(\n  props,\n  scroller,\n  scrollerEl,\n  {\n    onResize: () => emit('resize'),\n    onVisible: () => emit('visible'),\n  },\n)\n\nfunction getDefaultSlotBindings(itemWithSize: unknown, index: number, active: boolean) {\n  const typedItem = itemWithSize as ItemWithSize\n  return {\n    item: typedItem.item,\n    index,\n    active,\n    itemWithSize: typedItem,\n  }\n}\n\n// Expose\ndefineExpose({\n  scrollToItem,\n  scrollToBottom,\n  getItemSize,\n  forceUpdate,\n})\n</script>\n\n<template>\n  <RecycleScroller\n    ref=\"scroller\"\n    :items=\"itemsWithSize\"\n    :min-item-size=\"props.minItemSize\"\n    :direction=\"props.direction\"\n    key-field=\"id\"\n    :list-tag=\"props.listTag\"\n    :item-tag=\"props.itemTag\"\n    v-bind=\"$attrs\"\n    @resize=\"onScrollerResize\"\n    @visible=\"onScrollerVisible\"\n  >\n    <template #default=\"{ item: itemWithSize, index, active }\">\n      <slot v-bind=\"getDefaultSlotBindings(itemWithSize, index, active)\" />\n    </template>\n    <template\n      v-if=\"$slots.before\"\n      #before\n    >\n      <slot name=\"before\" />\n    </template>\n    <template\n      v-if=\"$slots.after\"\n      #after\n    >\n      <slot name=\"after\" />\n    </template>\n    <template #empty>\n      <slot name=\"empty\" />\n    </template>\n  </RecycleScroller>\n</template>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScrollerItem.spec.ts",
    "content": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { computed } from 'vue'\nimport DynamicScrollerItem from './DynamicScrollerItem.vue'\n\nconst mocks = vi.hoisted(() => {\n  return {\n    useDynamicScrollerItem: vi.fn(),\n  }\n})\n\nvi.mock('../composables/useDynamicScrollerItem', () => {\n  return {\n    useDynamicScrollerItem: mocks.useDynamicScrollerItem,\n  }\n})\n\ndescribe('dynamicScrollerItem', () => {\n  beforeEach(() => {\n    mocks.useDynamicScrollerItem.mockReset()\n    mocks.useDynamicScrollerItem.mockReturnValue({\n      id: computed(() => 'row-1'),\n      size: computed(() => 0),\n      finalActive: computed(() => true),\n      updateSize: vi.fn(),\n    })\n  })\n\n  it('renders the configured tag and wires props to useDynamicScrollerItem', () => {\n    const wrapper = mount(DynamicScrollerItem, {\n      props: {\n        item: { id: 'row-1' },\n        active: true,\n        watchData: true,\n        emitResize: true,\n        tag: 'section',\n      },\n      slots: {\n        default: 'content',\n      },\n    })\n\n    const [optionsArg, elRefArg] = mocks.useDynamicScrollerItem.mock.calls[0]\n\n    expect(wrapper.element.tagName).toBe('SECTION')\n    expect(wrapper.text()).toBe('content')\n    expect(optionsArg.item).toEqual({ id: 'row-1' })\n    expect(optionsArg.active).toBe(true)\n    expect(optionsArg.watchData).toBe(true)\n    expect(optionsArg.emitResize).toBe(true)\n    expect(elRefArg).toHaveProperty('value')\n  })\n\n  it('re-emits resize from composable callbacks', () => {\n    let onResize: ((id: string | number) => void) | undefined\n    mocks.useDynamicScrollerItem.mockImplementation((_options, _el, callbacks) => {\n      onResize = callbacks?.onResize\n      return {\n        id: computed(() => 'row-1'),\n        size: computed(() => 0),\n        finalActive: computed(() => true),\n        updateSize: vi.fn(),\n      }\n    })\n\n    const wrapper = mount(DynamicScrollerItem, {\n      props: {\n        item: { id: 'row-1' },\n        active: true,\n      },\n    })\n\n    onResize?.('row-1')\n\n    expect(wrapper.emitted('resize')).toEqual([['row-1']])\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/DynamicScrollerItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useDynamicScrollerItem } from '../composables/useDynamicScrollerItem'\n\nconst props = withDefaults(defineProps<{\n  item: unknown\n  watchData?: boolean\n  active: boolean\n  index?: number\n  sizeDependencies?: Record<string, unknown> | unknown[] | null\n  emitResize?: boolean\n  tag?: string\n}>(), {\n  watchData: false,\n  index: undefined,\n  sizeDependencies: null,\n  emitResize: false,\n  tag: 'div',\n})\n\nconst emit = defineEmits<{\n  resize: [id: string | number]\n}>()\n\ndefineSlots()\n\nconst el = ref<HTMLElement>()\n\nuseDynamicScrollerItem(\n  props,\n  el,\n  {\n    onResize: id => emit('resize', id),\n  },\n)\n</script>\n\n<template>\n  <component\n    :is=\"props.tag\"\n    ref=\"el\"\n  >\n    <slot />\n  </component>\n</template>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ItemView.spec.ts",
    "content": "import type { View } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it } from 'vitest'\nimport { h } from 'vue'\nimport ItemView from './ItemView.vue'\n\ndescribe('itemView', () => {\n  it('renders the configured tag and forwards slot props', () => {\n    const view: View = {\n      item: { label: 'Alpha' },\n      position: 0,\n      offset: 0,\n      nr: {\n        id: 1,\n        index: 2,\n        used: true,\n        key: 'a',\n        type: 'default',\n      },\n    }\n\n    const wrapper = mount(ItemView, {\n      props: {\n        view,\n        itemTag: 'li',\n      },\n      slots: {\n        default: ({ item, index, active }: any) => h('span', { class: 'slot-content' }, `${item.label}|${index}|${active}`),\n      },\n    })\n\n    expect(wrapper.element.tagName).toBe('LI')\n    expect(wrapper.find('.slot-content').text()).toBe('Alpha|2|true')\n  })\n\n  it('uses nr.used for the active slot prop', () => {\n    const wrapper = mount(ItemView, {\n      props: {\n        view: {\n          item: { label: 'Beta' },\n          position: 0,\n          offset: 0,\n          nr: {\n            id: 2,\n            index: 0,\n            used: false,\n            key: 'b',\n            type: 'default',\n          },\n        },\n        itemTag: 'div',\n      },\n      slots: {\n        default: ({ active }: any) => h('span', { class: 'active-flag' }, String(active)),\n      },\n    })\n\n    expect(wrapper.find('.active-flag').text()).toBe('false')\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ItemView.vue",
    "content": "<!-- Avoid re-renders of slots -->\n\n<script setup lang=\"ts\">\nimport type { View } from '../types'\n\nconst props = defineProps<{\n  view: View\n  itemTag: string\n}>()\n</script>\n\n<template>\n  <component\n    :is=\"props.itemTag\"\n    class=\"vue-recycle-scroller__item-view\"\n  >\n    <slot\n      :item=\"props.view.item\"\n      :index=\"props.view.nr.index\"\n      :active=\"props.view.nr.used\"\n    />\n  </component>\n</template>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/RecycleScroller.spec.ts",
    "content": "import { mount } from '@vue/test-utils'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { computed, defineComponent, h, nextTick, ref } from 'vue'\nimport RecycleScroller from './RecycleScroller.vue'\n\nconst mocks = vi.hoisted(() => {\n  return {\n    useRecycleScroller: vi.fn(),\n    scrollToItem: vi.fn(),\n    scrollToPosition: vi.fn(),\n    getScroll: vi.fn(),\n    updateVisibleItems: vi.fn(),\n    handleScroll: vi.fn(),\n    handleResize: vi.fn(),\n    handleVisibilityChange: vi.fn(),\n  }\n})\n\nvi.mock('../composables/useRecycleScroller', () => {\n  return {\n    useRecycleScroller: mocks.useRecycleScroller,\n  }\n})\n\nconst ResizeObserverStub = defineComponent({\n  name: 'ResizeObserver',\n  emits: ['notify'],\n  setup(_props, { emit }) {\n    return () => h('button', {\n      class: 'resize-observer-notify',\n      type: 'button',\n      onClick: () => emit('notify'),\n    })\n  },\n})\n\ndescribe('recycleScroller', () => {\n  beforeEach(() => {\n    mocks.useRecycleScroller.mockReset()\n    mocks.scrollToItem.mockReset()\n    mocks.scrollToPosition.mockReset()\n    mocks.getScroll.mockReset()\n    mocks.updateVisibleItems.mockReset()\n    mocks.handleScroll.mockReset()\n    mocks.handleResize.mockReset()\n    mocks.handleVisibilityChange.mockReset()\n\n    mocks.getScroll.mockReturnValue({ start: 0, end: 80 })\n    mocks.updateVisibleItems.mockReturnValue({ continuous: true })\n\n    mocks.useRecycleScroller.mockImplementation((_options, _el, _before, _after, callbacks) => {\n      return {\n        pool: ref([{\n          item: { id: 'a', label: 'Alpha' },\n          position: 0,\n          offset: 0,\n          nr: {\n            id: 1,\n            index: 0,\n            used: true,\n            key: 'a',\n            type: 'default',\n          },\n        }]),\n        totalSize: ref(320),\n        ready: ref(true),\n        sizes: computed(() => []),\n        simpleArray: computed(() => false),\n        scrollToItem: (index: number) => mocks.scrollToItem(index),\n        scrollToPosition: (position: number) => mocks.scrollToPosition(position),\n        getScroll: () => mocks.getScroll(),\n        updateVisibleItems: (itemsChanged: boolean, checkPositionDiff?: boolean) =>\n          mocks.updateVisibleItems(itemsChanged, checkPositionDiff),\n        handleScroll: () => mocks.handleScroll(),\n        handleResize: () => {\n          mocks.handleResize()\n          callbacks?.onResize?.()\n        },\n        handleVisibilityChange: (isVisible: boolean, entry: IntersectionObserverEntry) => {\n          mocks.handleVisibilityChange(isVisible, entry)\n          if (isVisible)\n            callbacks?.onVisible?.()\n          else\n            callbacks?.onHidden?.()\n        },\n        sortViews: vi.fn(),\n      }\n    })\n  })\n\n  it('renders slots and forwards default slot props from pool views', async () => {\n    const wrapper = mount(RecycleScroller, {\n      props: {\n        items: [{ id: 'a' }],\n        itemSize: 20,\n      },\n      slots: {\n        before: () => h('div', { class: 'before-slot' }, 'before'),\n        default: ({ item, index, active }: any) => h('div', { class: 'row' }, `${item.label}|${index}|${active}`),\n        empty: () => h('div', { class: 'empty-slot' }, 'empty'),\n        after: () => h('div', { class: 'after-slot' }, 'after'),\n      },\n      global: {\n        stubs: {\n          ResizeObserver: ResizeObserverStub,\n        },\n      },\n    })\n\n    await nextTick()\n\n    expect(wrapper.classes()).toContain('vue-recycle-scroller')\n    expect(wrapper.classes()).toContain('ready')\n    expect(wrapper.classes()).toContain('direction-vertical')\n    expect(wrapper.find('.before-slot').exists()).toBe(true)\n    expect(wrapper.find('.after-slot').exists()).toBe(true)\n    expect(wrapper.find('.empty-slot').exists()).toBe(false)\n    expect(wrapper.find('.row').text()).toBe('Alpha|0|true')\n    expect(wrapper.emitted('visible')).toHaveLength(1)\n  })\n\n  it('renders empty slot only when items is empty', async () => {\n    const wrapper = mount(RecycleScroller, {\n      props: {\n        items: [],\n        itemSize: 20,\n      },\n      slots: {\n        empty: () => h('div', { class: 'empty-slot' }, 'empty'),\n      },\n      global: {\n        stubs: {\n          ResizeObserver: ResizeObserverStub,\n        },\n      },\n    })\n\n    await nextTick()\n\n    expect(wrapper.find('.empty-slot').exists()).toBe(true)\n  })\n\n  it('wires scroll/resize handlers and exposes composable methods', async () => {\n    const wrapper = mount(RecycleScroller, {\n      props: {\n        items: [{ id: 'a' }],\n        itemSize: 20,\n      },\n      global: {\n        stubs: {\n          ResizeObserver: ResizeObserverStub,\n        },\n      },\n    })\n    const vm = wrapper.vm as any\n\n    await wrapper.trigger('scroll')\n    await wrapper.get('.resize-observer-notify').trigger('click')\n\n    expect(mocks.handleScroll).toHaveBeenCalledTimes(1)\n    expect(mocks.handleResize).toHaveBeenCalledTimes(1)\n    expect(wrapper.emitted('resize')).toHaveLength(1)\n\n    vm.scrollToItem(5)\n    vm.scrollToPosition(180)\n\n    expect(mocks.scrollToItem).toHaveBeenCalledWith(5)\n    expect(mocks.scrollToPosition).toHaveBeenCalledWith(180)\n    expect(vm.getScroll()).toEqual({ start: 0, end: 80 })\n  })\n\n  it('passes options to useRecycleScroller', () => {\n    mount(RecycleScroller, {\n      props: {\n        items: [{ id: 'a' }],\n        direction: 'horizontal',\n        keyField: 'id',\n        listTag: 'ul',\n        itemTag: 'li',\n        itemSize: 30,\n        buffer: 150,\n      },\n      global: {\n        stubs: {\n          ResizeObserver: ResizeObserverStub,\n        },\n      },\n    })\n\n    const [optionsArg] = mocks.useRecycleScroller.mock.calls[0]\n    expect(optionsArg.direction).toBe('horizontal')\n    expect(optionsArg.keyField).toBe('id')\n    expect(optionsArg.listTag).toBe('ul')\n    expect(optionsArg.itemTag).toBe('li')\n    expect(optionsArg.itemSize).toBe(30)\n    expect(optionsArg.buffer).toBe(150)\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/RecycleScroller.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ScrollDirection } from '../types'\nimport { ref } from 'vue'\nimport { useRecycleScroller } from '../composables/useRecycleScroller'\nimport { ObserveVisibility } from '../directives/observeVisibility'\nimport ItemView from './ItemView.vue'\nimport ResizeObserver from './ResizeObserver.vue'\n\nconst props = withDefaults(defineProps<{\n  items: unknown[]\n  keyField?: string\n  direction?: ScrollDirection\n  listTag?: string\n  itemTag?: string\n  itemSize?: number | null\n  gridItems?: number\n  itemSecondarySize?: number\n  minItemSize?: number | string | null\n  sizeField?: string\n  typeField?: string\n  buffer?: number\n  pageMode?: boolean\n  prerender?: number\n  emitUpdate?: boolean\n  disableTransform?: boolean\n  updateInterval?: number\n  skipHover?: boolean\n  listClass?: string | Record<string, boolean> | Array<string | Record<string, boolean>>\n  itemClass?: string | Record<string, boolean> | Array<string | Record<string, boolean>>\n}>(), {\n  keyField: 'id',\n  direction: 'vertical',\n  listTag: 'div',\n  itemTag: 'div',\n  itemSize: null,\n  gridItems: undefined,\n  itemSecondarySize: undefined,\n  minItemSize: null,\n  sizeField: 'size',\n  typeField: 'type',\n  buffer: 200,\n  pageMode: false,\n  prerender: 0,\n  emitUpdate: false,\n  disableTransform: false,\n  updateInterval: 0,\n  skipHover: false,\n  listClass: '',\n  itemClass: '',\n})\n\nconst emit = defineEmits<{\n  'resize': []\n  'visible': []\n  'hidden': []\n  'update': [startIndex: number, endIndex: number, visibleStartIndex: number, visibleEndIndex: number]\n  'scroll-start': []\n  'scroll-end': []\n}>()\n\nconst vObserveVisibility = ObserveVisibility\n\n// Template refs\nconst el = ref<HTMLElement>()\nconst before = ref<HTMLElement>()\nconst after = ref<HTMLElement>()\n\n// Hover state (UI-specific, not in composable)\nconst hoverKey = ref<string | number | null>(null)\n\nconst {\n  pool,\n  totalSize,\n  ready,\n  scrollToItem,\n  scrollToPosition,\n  getScroll,\n  updateVisibleItems,\n  handleScroll,\n  handleResize,\n  handleVisibilityChange,\n} = useRecycleScroller(\n  props,\n  el,\n  before,\n  after,\n  {\n    onResize: () => emit('resize'),\n    onVisible: () => emit('visible'),\n    onHidden: () => emit('hidden'),\n    onUpdate: (startIndex, endIndex, visibleStartIndex, visibleEndIndex) =>\n      emit('update', startIndex, endIndex, visibleStartIndex, visibleEndIndex),\n  },\n)\n\n// Expose public methods and el ref\ndefineExpose({\n  el,\n  scrollToItem,\n  scrollToPosition,\n  getScroll,\n  updateVisibleItems,\n})\n</script>\n\n<template>\n  <div\n    ref=\"el\"\n    v-observe-visibility=\"handleVisibilityChange\"\n    class=\"vue-recycle-scroller\"\n    :class=\"{\n      ready,\n      'page-mode': props.pageMode,\n      [`direction-${props.direction}`]: true,\n    }\"\n    @scroll.passive=\"handleScroll\"\n  >\n    <div\n      v-if=\"$slots.before\"\n      ref=\"before\"\n      class=\"vue-recycle-scroller__slot\"\n    >\n      <slot\n        name=\"before\"\n      />\n    </div>\n\n    <component\n      :is=\"props.listTag\"\n      :style=\"{ [props.direction === 'vertical' ? 'minHeight' : 'minWidth']: `${totalSize}px` }\"\n      class=\"vue-recycle-scroller__item-wrapper\"\n      :class=\"props.listClass\"\n    >\n      <ItemView\n        v-for=\"view of pool\"\n        :key=\"view.nr.id\"\n        :view=\"view\"\n        :item-tag=\"props.itemTag\"\n        :style=\"ready\n          ? [\n            (props.disableTransform\n              ? { [props.direction === 'vertical' ? 'top' : 'left']: `${view.position}px`, willChange: 'unset' }\n              : { transform: `translate${props.direction === 'vertical' ? 'Y' : 'X'}(${view.position}px) translate${props.direction === 'vertical' ? 'X' : 'Y'}(${view.offset}px)` }),\n            {\n              width: props.gridItems ? `${props.direction === 'vertical' ? props.itemSecondarySize || props.itemSize : props.itemSize}px` : undefined,\n              height: props.gridItems ? `${props.direction === 'horizontal' ? props.itemSecondarySize || props.itemSize : props.itemSize}px` : undefined,\n              visibility: view.nr.used ? 'visible' : 'hidden',\n            },\n          ]\n          : null\"\n        class=\"vue-recycle-scroller__item-view\"\n        :class=\"[\n          props.itemClass,\n          {\n            hover: !props.skipHover && hoverKey === view.nr.key,\n          },\n        ]\"\n        v-on=\"props.skipHover ? {} : {\n          mouseenter: () => { hoverKey = view.nr.key },\n          mouseleave: () => { hoverKey = null },\n        }\"\n      >\n        <template #default=\"slotProps\">\n          <slot v-bind=\"slotProps\" />\n        </template>\n      </ItemView>\n\n      <slot\n        v-if=\"props.items.length === 0\"\n        name=\"empty\"\n      />\n    </component>\n\n    <div\n      v-if=\"$slots.after\"\n      ref=\"after\"\n      class=\"vue-recycle-scroller__slot\"\n    >\n      <slot\n        name=\"after\"\n      />\n    </div>\n\n    <ResizeObserver @notify=\"handleResize\" />\n  </div>\n</template>\n\n<style>\n.vue-recycle-scroller {\n  position: relative;\n}\n\n.vue-recycle-scroller.direction-vertical:not(.page-mode) {\n  overflow-y: auto;\n}\n\n.vue-recycle-scroller.direction-horizontal:not(.page-mode) {\n  overflow-x: auto;\n}\n\n.vue-recycle-scroller.direction-horizontal {\n  display: flex;\n}\n\n.vue-recycle-scroller__slot {\n  flex: auto 0 0;\n}\n\n.vue-recycle-scroller__item-wrapper {\n  flex: 1;\n  box-sizing: border-box;\n  overflow: hidden;\n  position: relative;\n}\n\n.vue-recycle-scroller.ready .vue-recycle-scroller__item-view {\n  position: absolute;\n  top: 0;\n  left: 0;\n  will-change: transform;\n}\n\n.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper {\n  width: 100%;\n}\n\n.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper {\n  height: 100%;\n}\n\n.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view {\n  width: 100%;\n}\n\n.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view {\n  height: 100%;\n}\n</style>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/components/ResizeObserver.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\n\nconst emit = defineEmits<{\n  notify: []\n}>()\n\nconst el = ref<HTMLElement>()\n\nlet observer: ResizeObserver | null = null\nlet fallbackListener: (() => void) | null = null\n\nfunction notify() {\n  emit('notify')\n}\n\nonMounted(() => {\n  const target = el.value?.parentElement\n  if (!target)\n    return\n\n  if (typeof ResizeObserver !== 'undefined') {\n    observer = new ResizeObserver(() => {\n      notify()\n    })\n    observer.observe(target)\n    return\n  }\n\n  fallbackListener = () => notify()\n  window.addEventListener('resize', fallbackListener)\n})\n\nonBeforeUnmount(() => {\n  if (observer) {\n    observer.disconnect()\n    observer = null\n  }\n  if (fallbackListener) {\n    window.removeEventListener('resize', fallbackListener)\n    fallbackListener = null\n  }\n})\n</script>\n\n<template>\n  <div\n    ref=\"el\"\n    class=\"vue-recycle-scroller__resize-observer\"\n    aria-hidden=\"true\"\n  />\n</template>\n\n<style scoped>\n.vue-recycle-scroller__resize-observer {\n  position: absolute;\n  inset: 0;\n  opacity: 0;\n  pointer-events: none;\n  z-index: -1;\n}\n</style>\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScroller.spec.ts",
    "content": "import type { ScrollDirection } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it, vi } from 'vitest'\nimport { defineComponent, nextTick, reactive, ref } from 'vue'\nimport { useDynamicScroller } from './useDynamicScroller'\n\nfunction mountHarness(initialItems: unknown[]) {\n  const scrollToItemSpy = vi.fn()\n  const onResize = vi.fn()\n  const onVisible = vi.fn()\n  const options = reactive({\n    items: initialItems,\n    keyField: 'id',\n    direction: 'vertical' as ScrollDirection,\n    minItemSize: 20,\n  })\n  const scrollerRef = ref({\n    scrollToItem: scrollToItemSpy,\n  })\n  const el = ref(document.createElement('div'))\n\n  const Harness = defineComponent({\n    setup() {\n      const state = useDynamicScroller(options, scrollerRef, el, {\n        onResize,\n        onVisible,\n      })\n      return {\n        ...state,\n        options,\n        el,\n      }\n    },\n    template: '<div />',\n  })\n\n  const wrapper = mount(Harness)\n  return {\n    wrapper,\n    vm: wrapper.vm as any,\n    options,\n    el,\n    scrollToItemSpy,\n    onResize,\n    onVisible,\n  }\n}\n\ndescribe('useDynamicScroller', () => {\n  it('builds itemsWithSize and exposes item size helpers', async () => {\n    const items = [\n      { id: 'a', label: 'Alpha' },\n      { id: 'b', label: 'Beta' },\n    ]\n    const { vm, options } = mountHarness(items)\n\n    expect(vm.itemsWithSize).toHaveLength(2)\n    expect(vm.itemsWithSize[0].id).toBe('a')\n    expect(vm.itemsWithSize[0].size).toBe(0)\n    expect(vm.itemsWithSize[1].id).toBe('b')\n\n    vm.vscrollData.sizes.a = 42\n    await nextTick()\n\n    expect(vm.getItemSize(options.items[0])).toBe(42)\n  })\n\n  it('handles simple-array mode and callback forwarding', () => {\n    const { vm, onResize, onVisible } = mountHarness(['one', 'two'])\n\n    expect(vm.simpleArray).toBe(true)\n    expect(vm.itemsWithSize[1].id).toBe(1)\n\n    vm.vscrollData.sizes[1] = 30\n    expect(vm.getItemSize('two')).toBe(30)\n\n    vm.onScrollerResize()\n    vm.onScrollerVisible()\n\n    expect(onResize).toHaveBeenCalledTimes(1)\n    expect(onVisible).toHaveBeenCalledTimes(1)\n  })\n\n  it('forwards scrollToItem and clears sizes on direction change', async () => {\n    const { vm, options, scrollToItemSpy } = mountHarness([\n      { id: 'a' },\n      { id: 'b' },\n    ])\n\n    vm.scrollToItem(4)\n    expect(scrollToItemSpy).toHaveBeenCalledWith(4)\n\n    vm.vscrollData.sizes.a = 10\n    vm.vscrollData.sizes.b = 15\n    expect(Object.keys(vm.vscrollData.sizes)).toHaveLength(2)\n\n    options.direction = 'horizontal'\n    await nextTick()\n\n    expect(Object.keys(vm.vscrollData.sizes)).toHaveLength(0)\n  })\n\n  it('returns early on scrollToBottom when no scroller element is available', () => {\n    const { vm, el } = mountHarness([{ id: 'a' }])\n    el.value = undefined as any\n\n    expect(() => vm.scrollToBottom()).not.toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScroller.ts",
    "content": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from 'vue'\nimport type { ItemWithSize, ScrollDirection, VScrollData } from '../types'\nimport mitt from 'mitt'\nimport { computed, nextTick, onActivated, onDeactivated, onUnmounted, provide, reactive, toValue, watch } from 'vue'\n\nexport interface UseDynamicScrollerOptions {\n  items: unknown[]\n  keyField: string\n  direction: ScrollDirection\n  minItemSize: number | string\n}\n\nexport interface UseDynamicScrollerReturn {\n  vscrollData: VScrollData\n  itemsWithSize: ComputedRef<ItemWithSize[]>\n  simpleArray: ComputedRef<boolean>\n  resizeObserver: ResizeObserver | undefined\n  forceUpdate: (clear?: boolean) => void\n  scrollToItem: (index: number) => void\n  getItemSize: (item: unknown, index?: number) => number\n  scrollToBottom: () => void\n  onScrollerResize: () => void\n  onScrollerVisible: () => void\n}\n\nexport function useDynamicScroller(\n  options: MaybeRefOrGetter<UseDynamicScrollerOptions>,\n  scrollerRef: MaybeRef<{ scrollToItem: (index: number) => void } | undefined>,\n  el: MaybeRef<HTMLElement | undefined>,\n  callbacks?: {\n    onResize?: () => void\n    onVisible?: () => void\n  },\n): UseDynamicScrollerReturn {\n  // Internal state (non-reactive)\n  let _undefinedSizes = 0\n  let _undefinedMap: Record<string | number, boolean | undefined> = {}\n  const _events = mitt()\n  let _scrollingToBottom = false\n  let _resizeObserver: ResizeObserver | undefined\n\n  // Reactive state\n  const vscrollData = reactive<VScrollData>({\n    active: true,\n    sizes: {},\n    keyField: toValue(options).keyField,\n    simpleArray: false,\n  })\n\n  // ResizeObserver setup\n  if (typeof ResizeObserver !== 'undefined') {\n    _resizeObserver = new ResizeObserver((entries) => {\n      requestAnimationFrame(() => {\n        if (!Array.isArray(entries)) {\n          return\n        }\n        for (const entry of entries) {\n          if (entry.target && (entry.target as any).$_vs_onResize) {\n            let width: number, height: number\n            if (entry.borderBoxSize) {\n              const resizeObserverSize = entry.borderBoxSize[0]\n              width = resizeObserverSize.inlineSize\n              height = resizeObserverSize.blockSize\n            }\n            else {\n              width = entry.contentRect.width\n              height = entry.contentRect.height\n            }\n            ;(entry.target as any).$_vs_onResize((entry.target as any).$_vs_id, width, height)\n          }\n        }\n      })\n    })\n  }\n\n  // Provide\n  provide('vscrollData', vscrollData)\n  provide('vscrollParent', {\n    get $_undefinedSizes() { return _undefinedSizes },\n    set $_undefinedSizes(v: number) { _undefinedSizes = v },\n    get $_undefinedMap() { return _undefinedMap },\n    set $_undefinedMap(v: Record<string | number, boolean | undefined>) { _undefinedMap = v },\n    $_events: _events,\n    direction: computed(() => toValue(options).direction),\n  })\n  provide('vscrollResizeObserver', _resizeObserver)\n\n  // Computed\n  const simpleArray = computed(() => {\n    const opts = toValue(options)\n    return opts.items.length > 0 && typeof opts.items[0] !== 'object'\n  })\n\n  const itemsWithSize = computed<ItemWithSize[]>(() => {\n    const result: ItemWithSize[] = []\n    const opts = toValue(options)\n    const { items, keyField } = opts\n    const simple = simpleArray.value\n    const sizes = vscrollData.sizes\n    const l = items.length\n    for (let i = 0; i < l; i++) {\n      const item = items[i]\n      const id = simple ? i : (item as any)[keyField]\n      let size: number | undefined = sizes[id]\n      if (typeof size === 'undefined' && !_undefinedMap[id]) {\n        size = 0\n      }\n      result.push({\n        item,\n        id,\n        size,\n      })\n    }\n    return result\n  })\n\n  // Methods\n  function onScrollerResize() {\n    if (toValue(scrollerRef)) {\n      forceUpdate()\n    }\n    callbacks?.onResize?.()\n  }\n\n  function onScrollerVisible() {\n    _events.emit('vscroll:update', { force: false })\n    callbacks?.onVisible?.()\n  }\n\n  function forceUpdate(clear = false) {\n    if (clear || simpleArray.value) {\n      vscrollData.sizes = {}\n    }\n    _events.emit('vscroll:update', { force: true })\n  }\n\n  function scrollToItem(index: number) {\n    const scroller = toValue(scrollerRef)\n    if (scroller)\n      scroller.scrollToItem(index)\n  }\n\n  function getItemSize(item: unknown, index?: number): number {\n    const opts = toValue(options)\n    const id = simpleArray.value ? (index ?? opts.items.indexOf(item)) : (item as any)[opts.keyField]\n    return vscrollData.sizes[id] || 0\n  }\n\n  function scrollToBottom() {\n    const elValue = toValue(el)\n    if (!elValue)\n      return\n    if (_scrollingToBottom)\n      return\n    _scrollingToBottom = true\n    // Item is inserted to the DOM\n    nextTick(() => {\n      elValue.scrollTop = elValue.scrollHeight + 5000\n      // Item sizes are computed\n      const cb = () => {\n        elValue.scrollTop = elValue.scrollHeight + 5000\n        requestAnimationFrame(() => {\n          elValue.scrollTop = elValue.scrollHeight + 5000\n          if (_undefinedSizes === 0) {\n            _scrollingToBottom = false\n          }\n          else {\n            requestAnimationFrame(cb)\n          }\n        })\n      }\n      requestAnimationFrame(cb)\n    })\n  }\n\n  // Watchers\n  watch(() => toValue(options).items, () => {\n    forceUpdate()\n  })\n\n  watch(simpleArray, (value) => {\n    vscrollData.simpleArray = value\n  }, { immediate: true })\n\n  watch(() => toValue(options).direction, () => {\n    forceUpdate(true)\n  })\n\n  watch(itemsWithSize, (next, prev) => {\n    const elValue = toValue(el)\n    if (!elValue)\n      return\n    const scrollTop = elValue.scrollTop\n\n    // Calculate total diff between prev and next sizes\n    // over current scroll top. Then add it to scrollTop to\n    // avoid jumping the contents that the user is seeing.\n    const opts = toValue(options)\n    let prevActiveTop = 0\n    let activeTop = 0\n    const length = Math.min(next.length, prev.length)\n    for (let i = 0; i < length; i++) {\n      if (prevActiveTop >= scrollTop) {\n        break\n      }\n      prevActiveTop += (prev[i].size || opts.minItemSize as number)\n      activeTop += (next[i].size || opts.minItemSize as number)\n    }\n    const offset = activeTop - prevActiveTop\n\n    if (offset === 0) {\n      return\n    }\n\n    elValue.scrollTop += offset\n  })\n\n  // Lifecycle\n  onActivated(() => {\n    vscrollData.active = true\n  })\n\n  onDeactivated(() => {\n    vscrollData.active = false\n  })\n\n  onUnmounted(() => {\n    _events.all.clear()\n  })\n\n  return {\n    vscrollData,\n    itemsWithSize,\n    simpleArray,\n    resizeObserver: _resizeObserver,\n    forceUpdate,\n    scrollToItem,\n    getItemSize,\n    scrollToBottom,\n    onScrollerResize,\n    onScrollerVisible,\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useDynamicScrollerItem.ts",
    "content": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from 'vue'\nimport type { VScrollData } from '../types'\nimport { computed, inject, nextTick, onBeforeUnmount, onMounted, toValue, watch } from 'vue'\n\nexport interface UseDynamicScrollerItemOptions {\n  item: unknown\n  watchData: boolean\n  active: boolean\n  index?: number\n  sizeDependencies?: Record<string, unknown> | unknown[] | null\n  emitResize: boolean\n}\n\nexport interface UseDynamicScrollerItemReturn {\n  id: ComputedRef<string | number>\n  size: ComputedRef<number>\n  finalActive: ComputedRef<boolean>\n  updateSize: () => void\n}\n\nexport function useDynamicScrollerItem(\n  options: MaybeRefOrGetter<UseDynamicScrollerItemOptions>,\n  el: MaybeRef<HTMLElement | undefined>,\n  callbacks?: {\n    onResize?: (id: string | number) => void\n  },\n): UseDynamicScrollerItemReturn {\n  const vscrollData = inject<VScrollData>('vscrollData')!\n  const vscrollParent = inject<any>('vscrollParent')!\n  const vscrollResizeObserver = inject<ResizeObserver | undefined>('vscrollResizeObserver')\n\n  // Internal state\n  let _forceNextVScrollUpdate: string | number | null = null\n  let _pendingSizeUpdate: string | number | null = null\n  let _pendingVScrollUpdate: string | number | null = null\n  let _sizeObserved = false\n  let _watchDataStop: (() => void) | null = null\n\n  // Computed\n  const id = computed<string | number>(() => {\n    const opts = toValue(options)\n    if (vscrollData.simpleArray)\n      return opts.index!\n    if (vscrollData.keyField in (opts.item as any))\n      return (opts.item as any)[vscrollData.keyField]\n    throw new Error(`keyField '${vscrollData.keyField}' not found in your item. You should set a valid keyField prop on your Scroller`)\n  })\n\n  const size = computed(() => {\n    return vscrollData.sizes[id.value] || 0\n  })\n\n  const finalActive = computed(() => {\n    return toValue(options).active && vscrollData.active\n  })\n\n  // Methods\n  function updateSize() {\n    if (finalActive.value) {\n      if (_pendingSizeUpdate !== id.value) {\n        _pendingSizeUpdate = id.value\n        _forceNextVScrollUpdate = null\n        _pendingVScrollUpdate = null\n        computeSize(id.value)\n      }\n    }\n    else {\n      _forceNextVScrollUpdate = id.value\n    }\n  }\n\n  function updateWatchData() {\n    const opts = toValue(options)\n    if (opts.watchData && !vscrollResizeObserver) {\n      _watchDataStop = watch(() => toValue(options).item, () => {\n        onDataUpdate()\n      }, {\n        deep: true,\n      })\n    }\n    else if (_watchDataStop) {\n      _watchDataStop()\n      _watchDataStop = null\n    }\n  }\n\n  function onVscrollUpdate({ force }: { force: boolean }) {\n    // If not active, schedule a size update when it becomes active\n    if (!finalActive.value && force) {\n      _pendingVScrollUpdate = id.value\n    }\n\n    if (_forceNextVScrollUpdate === id.value || force || !size.value) {\n      updateSize()\n    }\n  }\n\n  function onDataUpdate() {\n    updateSize()\n  }\n\n  function computeSize(targetId: string | number) {\n    nextTick(() => {\n      if (id.value === targetId) {\n        const elValue = toValue(el)\n        if (!elValue)\n          return\n        const width = elValue.offsetWidth\n        const height = elValue.offsetHeight\n        applyWidthHeight(width, height)\n      }\n      _pendingSizeUpdate = null\n    })\n  }\n\n  function applyWidthHeight(width: number, height: number) {\n    const direction = typeof vscrollParent.direction === 'object'\n      ? (vscrollParent.direction as ComputedRef<string>).value\n      : vscrollParent.direction\n    const sizeValue = ~~(direction === 'vertical' ? height : width)\n    if (sizeValue && size.value !== sizeValue) {\n      applySize(sizeValue)\n    }\n  }\n\n  function applySize(sizeValue: number) {\n    if (vscrollParent.$_undefinedMap[id.value]) {\n      vscrollParent.$_undefinedSizes--\n      vscrollParent.$_undefinedMap[id.value] = undefined\n    }\n    vscrollData.sizes[id.value] = sizeValue\n    if (toValue(options).emitResize)\n      callbacks?.onResize?.(id.value)\n  }\n\n  function observeSize() {\n    if (!vscrollResizeObserver)\n      return\n    if (_sizeObserved)\n      return\n    const elValue = toValue(el)\n    if (!elValue)\n      return\n    vscrollResizeObserver.observe(elValue)\n    ;(elValue as any).$_vs_id = id.value\n    ;(elValue as any).$_vs_onResize = onResize\n    _sizeObserved = true\n  }\n\n  function unobserveSize() {\n    if (!vscrollResizeObserver)\n      return\n    if (!_sizeObserved)\n      return\n    const elValue = toValue(el)\n    if (!elValue)\n      return\n    vscrollResizeObserver.unobserve(elValue)\n    ;(elValue as any).$_vs_onResize = undefined\n    _sizeObserved = false\n  }\n\n  function onResize(targetId: string | number, width: number, height: number) {\n    if (id.value === targetId) {\n      applyWidthHeight(width, height)\n    }\n  }\n\n  // Watchers\n  watch(() => toValue(options).watchData, () => {\n    updateWatchData()\n  })\n\n  watch(id, (value, oldValue) => {\n    const elValue = toValue(el)\n    if (elValue) {\n      ;(elValue as any).$_vs_id = id.value\n    }\n    if (!size.value) {\n      onDataUpdate()\n    }\n\n    if (_sizeObserved) {\n      // In case the old item had the same size, it won't trigger the ResizeObserver\n      // since we are reusing the same DOM node\n      const oldSize = vscrollData.sizes[oldValue]\n      const newSize = vscrollData.sizes[value]\n\n      if (newSize != null && newSize !== oldSize) {\n        applySize(newSize)\n      }\n      else if (oldSize != null && oldSize !== newSize) {\n        applySize(oldSize)\n      }\n    }\n  })\n\n  watch(finalActive, (value) => {\n    if (!size.value) {\n      if (value) {\n        if (!vscrollParent.$_undefinedMap[id.value]) {\n          vscrollParent.$_undefinedSizes++\n          vscrollParent.$_undefinedMap[id.value] = true\n        }\n      }\n      else {\n        if (vscrollParent.$_undefinedMap[id.value]) {\n          vscrollParent.$_undefinedSizes--\n          vscrollParent.$_undefinedMap[id.value] = false\n        }\n      }\n    }\n\n    if (vscrollResizeObserver) {\n      if (value) {\n        observeSize()\n      }\n      else {\n        unobserveSize()\n      }\n    }\n    else if (value && _pendingVScrollUpdate === id.value) {\n      updateSize()\n    }\n  })\n\n  // Created logic\n  updateWatchData()\n\n  if (!vscrollResizeObserver) {\n    const opts = toValue(options)\n    if (opts.sizeDependencies) {\n      for (const k in opts.sizeDependencies) {\n        watch(() => (toValue(options).sizeDependencies as any)[k], onDataUpdate)\n      }\n    }\n\n    vscrollParent.$_events.on('vscroll:update', onVscrollUpdate)\n  }\n\n  // Lifecycle\n  onMounted(() => {\n    if (finalActive.value) {\n      updateSize()\n      observeSize()\n    }\n  })\n\n  onBeforeUnmount(() => {\n    vscrollParent.$_events.off('vscroll:update', onVscrollUpdate)\n    unobserveSize()\n  })\n\n  return {\n    id,\n    size,\n    finalActive,\n    updateSize,\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useIdState.spec.ts",
    "content": "import { mount } from '@vue/test-utils'\nimport { describe, expect, it } from 'vitest'\nimport { defineComponent, nextTick } from 'vue'\nimport { useIdState } from './useIdState'\n\ndescribe('useIdState', () => {\n  it('throws when called outside setup', () => {\n    expect(() => useIdState()).toThrow('[useIdState] Must be called inside setup()')\n  })\n\n  it('keeps independent state per item id', async () => {\n    const Harness = defineComponent({\n      props: {\n        item: {\n          type: Object as () => { id: number, label: string },\n          required: true,\n        },\n      },\n      idState() {\n        return {\n          count: 0,\n          label: (this as any).item.label,\n        }\n      },\n      setup() {\n        const { idState } = useIdState()\n        return { idState }\n      },\n      template: '<div />',\n    })\n\n    const wrapper = mount(Harness, {\n      props: {\n        item: { id: 1, label: 'first' },\n      },\n    })\n    const vm = wrapper.vm as any\n\n    vm.idState.count = 3\n\n    await wrapper.setProps({ item: { id: 2, label: 'second' } })\n    await nextTick()\n\n    expect(vm.idState.count).toBe(0)\n    expect(vm.idState.label).toBe('second')\n\n    vm.idState.count = 8\n    await wrapper.setProps({ item: { id: 1, label: 'first-updated' } })\n    await nextTick()\n\n    expect(vm.idState.count).toBe(3)\n    expect(vm.idState.label).toBe('first')\n  })\n\n  it('supports string idProp values', async () => {\n    const Harness = defineComponent({\n      props: {\n        rowId: {\n          type: Number,\n          required: true,\n        },\n      },\n      idState() {\n        return {\n          hits: 1,\n        }\n      },\n      setup() {\n        const { idState } = useIdState({ idProp: 'rowId' })\n        return { idState }\n      },\n      template: '<div />',\n    })\n\n    const wrapper = mount(Harness, {\n      props: {\n        rowId: 10,\n      },\n    })\n    const vm = wrapper.vm as any\n\n    vm.idState.hits = 4\n\n    await wrapper.setProps({ rowId: 11 })\n    await nextTick()\n\n    expect(vm.idState.hits).toBe(1)\n\n    await wrapper.setProps({ rowId: 10 })\n    await nextTick()\n\n    expect(vm.idState.hits).toBe(4)\n  })\n\n  it('throws when idState option is missing', () => {\n    const MissingFactory = defineComponent({\n      props: {\n        item: {\n          type: Object as () => { id: number },\n          required: true,\n        },\n      },\n      setup() {\n        useIdState()\n        return {}\n      },\n      template: '<div />',\n    })\n\n    expect(() => mount(MissingFactory, {\n      props: {\n        item: { id: 1 },\n      },\n    })).toThrow('[useIdState] Missing `idState` function on component definition.')\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useIdState.ts",
    "content": "import { getCurrentInstance, nextTick, onBeforeUpdate, reactive, ref, watch } from 'vue'\n\ntype IdPropFn = (vm: any) => string | number\n\nexport function useIdState({\n  idProp = (vm: any) => vm.item.id,\n}: { idProp?: IdPropFn | string } = {}) {\n  const store = reactive<Record<string | number, unknown>>({})\n  const idState = ref<unknown>(null)\n  let currentId: string | number | null = null\n\n  const instance = getCurrentInstance()!\n  if (!instance) {\n    throw new Error('[useIdState] Must be called inside setup()')\n  }\n\n  const getId: () => string | number = typeof idProp === 'function'\n    ? () => idProp(instance.proxy)\n    : () => (instance.proxy as any)[idProp]\n\n  function idStateInit(id: string | number): unknown {\n    const idStateFactory = (instance.proxy as any).$options.idState\n    if (typeof idStateFactory === 'function') {\n      const data = idStateFactory.call(instance.proxy, instance.proxy)\n      store[id] = data\n      currentId = id\n      return data\n    }\n    else {\n      throw new TypeError('[useIdState] Missing `idState` function on component definition.')\n    }\n  }\n\n  function updateIdState() {\n    const id = getId()\n    if (id == null) {\n      console.warn(`No id found for IdState with idProp: '${idProp}'.`)\n    }\n    if (id !== currentId) {\n      if (!store[id]) {\n        idStateInit(id)\n      }\n      idState.value = store[id]\n    }\n  }\n\n  watch(getId, (value) => {\n    nextTick(() => {\n      currentId = value\n    })\n  }, { immediate: true })\n\n  updateIdState()\n\n  onBeforeUpdate(() => {\n    updateIdState()\n  })\n\n  return {\n    idState,\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useRecycleScroller.spec.ts",
    "content": "import type { View } from '../types'\nimport { mount } from '@vue/test-utils'\nimport { describe, expect, it, vi } from 'vitest'\nimport { defineComponent, nextTick, reactive, ref } from 'vue'\nimport { useRecycleScroller } from './useRecycleScroller'\n\nfunction createView(index: number, used = true): View {\n  return {\n    item: { id: index },\n    position: 0,\n    offset: 0,\n    nr: {\n      id: index,\n      index,\n      used,\n      key: index,\n      type: 'default',\n    },\n  }\n}\n\nfunction mountHarness() {\n  const onUpdate = vi.fn()\n  const options = reactive({\n    items: Array.from({ length: 6 }, (_, id) => ({ id })),\n    keyField: 'id',\n    direction: 'vertical' as const,\n    itemSize: 10,\n    gridItems: undefined,\n    itemSecondarySize: undefined,\n    minItemSize: null,\n    sizeField: 'size',\n    typeField: 'type',\n    buffer: 0,\n    pageMode: false,\n    prerender: 0,\n    emitUpdate: true,\n    updateInterval: 0,\n  })\n\n  const Harness = defineComponent({\n    setup() {\n      const el = ref<HTMLElement>()\n      const state = useRecycleScroller(options, el, undefined, undefined, {\n        onUpdate,\n      })\n\n      return {\n        ...state,\n        el,\n      }\n    },\n    template: '<div ref=\"el\" style=\"height: 100px; overflow-y: auto;\" />',\n  })\n\n  const wrapper = mount(Harness)\n\n  return {\n    wrapper,\n    vm: wrapper.vm as any,\n    onUpdate,\n  }\n}\n\ndescribe('useRecycleScroller', () => {\n  it('does not refresh when visible views remain contiguous after sorting', async () => {\n    const { vm, onUpdate } = mountHarness()\n\n    await nextTick()\n    await nextTick()\n    onUpdate.mockClear()\n\n    vm.pool = [\n      createView(3, true),\n      createView(1, false),\n      createView(4, true),\n    ]\n\n    vm.sortViews()\n\n    expect(onUpdate).not.toHaveBeenCalled()\n  })\n\n  it('refreshes when sorting reveals a gap between visible views', async () => {\n    const { vm, onUpdate } = mountHarness()\n\n    await nextTick()\n    await nextTick()\n    onUpdate.mockClear()\n\n    vm.pool = [\n      createView(3, true),\n      createView(1, false),\n      createView(5, true),\n    ]\n\n    vm.sortViews()\n\n    expect(onUpdate).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/composables/useRecycleScroller.ts",
    "content": "import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue'\nimport type { ScrollDirection, ScrollState, Sizes, View, ViewNonReactive } from '../types'\nimport { computed, markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowReactive, toValue, watch } from 'vue'\nimport config from '../config'\nimport { getScrollParent } from '../scrollparent'\nimport { supportsPassive } from '../utils'\n\nexport interface UseRecycleScrollerOptions {\n  items: unknown[]\n  keyField: string\n  direction: ScrollDirection\n  itemSize: number | null\n  gridItems?: number\n  itemSecondarySize?: number\n  minItemSize: number | string | null\n  sizeField: string\n  typeField: string\n  buffer: number\n  pageMode: boolean\n  prerender: number\n  emitUpdate: boolean\n  updateInterval: number\n}\n\nexport interface UseRecycleScrollerReturn {\n  pool: Ref<View[]>\n  totalSize: Ref<number>\n  ready: Ref<boolean>\n  sizes: ComputedRef<Sizes | never[]>\n  simpleArray: ComputedRef<boolean>\n  scrollToItem: (index: number) => void\n  scrollToPosition: (position: number) => void\n  getScroll: () => ScrollState\n  updateVisibleItems: (itemsChanged: boolean, checkPositionDiff?: boolean) => { continuous: boolean }\n  handleScroll: () => void\n  handleResize: () => void\n  handleVisibilityChange: (isVisible: boolean, entry: IntersectionObserverEntry) => void\n  sortViews: () => void\n}\n\nlet uid = 0\n\nexport function useRecycleScroller(\n  options: MaybeRefOrGetter<UseRecycleScrollerOptions>,\n  el: MaybeRef<HTMLElement | undefined>,\n  before?: MaybeRef<HTMLElement | undefined>,\n  after?: MaybeRef<HTMLElement | undefined>,\n  callbacks?: {\n    onResize?: () => void\n    onVisible?: () => void\n    onHidden?: () => void\n    onUpdate?: (startIndex: number, endIndex: number, visibleStartIndex: number, visibleEndIndex: number) => void\n  },\n): UseRecycleScrollerReturn {\n  // Reactive state\n  const pool = ref<View[]>([])\n  const totalSize = ref(0)\n  const ready = ref(false)\n\n  // Internal state (non-reactive)\n  let _startIndex = 0\n  let _endIndex = 0\n  const _views = new Map<string | number, View>()\n  const _recycledPools = new Map<unknown, View[]>()\n  let _scrollDirty = false\n  let _lastUpdateScrollPosition = 0\n  let _prerender = false\n  let _updateTimeout: ReturnType<typeof setTimeout> | null = null\n  let _refreshTimout: ReturnType<typeof setTimeout> | null = null\n  let _sortTimer: ReturnType<typeof setTimeout> | null = null\n  let _computedMinItemSize = 0\n  let _listenerTarget: (Window | Element) | null = null\n\n  // Computed\n  const simpleArray = computed(() => {\n    const opts = toValue(options)\n    return opts.items.length > 0 && typeof opts.items[0] !== 'object'\n  })\n\n  const sizes = computed<Sizes | never[]>(() => {\n    const opts = toValue(options)\n    if (opts.itemSize === null) {\n      const sizes: Sizes = {\n        [-1]: { accumulator: 0 },\n      }\n      const items = opts.items\n      const field = opts.sizeField\n      const minItemSize = opts.minItemSize as number\n      let computedMinSize = 10000\n      let accumulator = 0\n      let current: number\n      for (let i = 0, l = items.length; i < l; i++) {\n        current = (items[i] as any)[field] || minItemSize\n        if (current < computedMinSize) {\n          computedMinSize = current\n        }\n        accumulator += current\n        sizes[i] = { accumulator, size: current }\n      }\n      _computedMinItemSize = computedMinSize\n      return sizes\n    }\n    return []\n  })\n\n  // Methods\n  function getRecycledPool(type: unknown): View[] {\n    let recycledPool = _recycledPools.get(type)\n    if (!recycledPool) {\n      recycledPool = []\n      _recycledPools.set(type, recycledPool)\n    }\n    return recycledPool\n  }\n\n  function createView(viewPool: View[], index: number, item: unknown, key: string | number, type: unknown): View {\n    const nr: ViewNonReactive = markRaw({\n      id: uid++,\n      index,\n      used: true,\n      key,\n      type,\n    })\n    const view: View = shallowReactive({\n      item,\n      position: 0,\n      offset: 0,\n      nr,\n    })\n    viewPool.push(view)\n    return view\n  }\n\n  function getRecycledView(type: unknown): View | undefined {\n    const recycledPool = getRecycledPool(type)\n    if (recycledPool && recycledPool.length) {\n      const view = recycledPool.pop()!\n      view.nr.used = true\n      return view\n    }\n    return undefined\n  }\n\n  function removeAndRecycleView(view: View) {\n    const type = view.nr.type\n    const recycledPool = getRecycledPool(type)\n    recycledPool.push(view)\n    view.nr.used = false\n    view.position = -9999\n    _views.delete(view.nr.key)\n  }\n\n  function removeAndRecycleAllViews() {\n    _views.clear()\n    _recycledPools.clear()\n    for (let i = 0, l = pool.value.length; i < l; i++) {\n      removeAndRecycleView(pool.value[i])\n    }\n  }\n\n  function handleResize() {\n    callbacks?.onResize?.()\n    if (ready.value)\n      updateVisibleItems(false)\n  }\n\n  function handleScroll() {\n    const opts = toValue(options)\n    if (!_scrollDirty) {\n      _scrollDirty = true\n      if (_updateTimeout)\n        return\n\n      const requestUpdate = () => requestAnimationFrame(() => {\n        _scrollDirty = false\n        const { continuous } = updateVisibleItems(false, true)\n\n        // It seems sometimes chrome doesn't fire scroll event :/\n        // When non continuous scrolling is ending, we force a refresh\n        if (!continuous) {\n          if (_refreshTimout)\n            clearTimeout(_refreshTimout)\n          _refreshTimout = setTimeout(handleScroll, opts.updateInterval + 100)\n        }\n      })\n\n      requestUpdate()\n\n      // Schedule the next update with throttling\n      if (opts.updateInterval) {\n        _updateTimeout = setTimeout(() => {\n          _updateTimeout = null\n          if (_scrollDirty)\n            requestUpdate()\n        }, opts.updateInterval)\n      }\n    }\n  }\n\n  function handleVisibilityChange(isVisible: boolean, entry: IntersectionObserverEntry) {\n    if (ready.value) {\n      if (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0) {\n        callbacks?.onVisible?.()\n        requestAnimationFrame(() => {\n          updateVisibleItems(false)\n        })\n      }\n      else {\n        callbacks?.onHidden?.()\n      }\n    }\n  }\n\n  function getListenerTarget(): Window | Element {\n    const target: Element | undefined = getScrollParent(toValue(el)!)\n    // Fix global scroll target for Chrome and Safari\n    if (window.document && (target === window.document.documentElement || target === window.document.body)) {\n      return window\n    }\n    return target || window\n  }\n\n  function getScroll(): ScrollState {\n    const elValue = toValue(el)!\n    const opts = toValue(options)\n    const isVertical = opts.direction === 'vertical'\n    let scrollState: ScrollState\n\n    if (opts.pageMode) {\n      const bounds = elValue.getBoundingClientRect()\n      const boundsSize = isVertical ? bounds.height : bounds.width\n      let start = -(isVertical ? bounds.top : bounds.left)\n      let size = isVertical ? window.innerHeight : window.innerWidth\n      if (start < 0) {\n        size += start\n        start = 0\n      }\n      if (start + size > boundsSize) {\n        size = boundsSize - start\n      }\n      scrollState = {\n        start,\n        end: start + size,\n      }\n    }\n    else if (isVertical) {\n      scrollState = {\n        start: elValue.scrollTop,\n        end: elValue.scrollTop + elValue.clientHeight,\n      }\n    }\n    else {\n      scrollState = {\n        start: elValue.scrollLeft,\n        end: elValue.scrollLeft + elValue.clientWidth,\n      }\n    }\n\n    return scrollState\n  }\n\n  function applyPageMode() {\n    const opts = toValue(options)\n    if (opts.pageMode) {\n      addListeners()\n    }\n    else {\n      removeListeners()\n    }\n  }\n\n  function addListeners() {\n    _listenerTarget = getListenerTarget()\n    _listenerTarget.addEventListener('scroll', handleScroll, supportsPassive()\n      ? { passive: true }\n      : false)\n    _listenerTarget.addEventListener('resize', handleResize as EventListener)\n  }\n\n  function removeListeners() {\n    if (!_listenerTarget) {\n      return\n    }\n\n    _listenerTarget.removeEventListener('scroll', handleScroll)\n    _listenerTarget.removeEventListener('resize', handleResize as EventListener)\n\n    _listenerTarget = null\n  }\n\n  function updateVisibleItems(itemsChanged: boolean, checkPositionDiff = false): { continuous: boolean } {\n    const opts = toValue(options)\n    const itemSize = opts.itemSize\n    const gridItems = opts.gridItems || 1\n    const itemSecondarySize = opts.itemSecondarySize || (itemSize as number)\n    const minItemSize = _computedMinItemSize\n    const typeField = opts.typeField\n    const keyField = simpleArray.value ? null : opts.keyField\n    const items = opts.items\n    const count = items.length\n    const sizesValue = sizes.value as Sizes\n    const views = _views\n    const poolValue = pool.value\n    let startIndex: number, endIndex: number\n    let totalSizeValue: number\n    let visibleStartIndex: number, visibleEndIndex: number\n\n    if (!count) {\n      startIndex = endIndex = visibleStartIndex = visibleEndIndex = totalSizeValue = 0\n    }\n    else if (_prerender) {\n      startIndex = visibleStartIndex = 0\n      endIndex = visibleEndIndex = Math.min(opts.prerender, items.length)\n      totalSizeValue = 0\n    }\n    else {\n      const scroll = getScroll()\n\n      // Skip update if user hasn't scrolled enough\n      if (checkPositionDiff) {\n        let positionDiff = scroll.start - _lastUpdateScrollPosition\n        if (positionDiff < 0)\n          positionDiff = -positionDiff\n        if ((itemSize === null && positionDiff < minItemSize) || (itemSize !== null && positionDiff < itemSize)) {\n          return {\n            continuous: true,\n          }\n        }\n      }\n      _lastUpdateScrollPosition = scroll.start\n\n      const buffer = opts.buffer\n      scroll.start -= buffer\n      scroll.end += buffer\n\n      // account for leading slot\n      let beforeSize = 0\n      const beforeEl = toValue(before)\n      if (beforeEl) {\n        beforeSize = beforeEl.scrollHeight\n        scroll.start -= beforeSize\n      }\n\n      // account for trailing slot\n      const afterEl = toValue(after)\n      if (afterEl) {\n        const afterSize = afterEl.scrollHeight\n        scroll.end += afterSize\n      }\n\n      // Variable size mode\n      if (itemSize === null) {\n        let h: number\n        let a = 0\n        let b = count - 1\n        let i = ~~(count / 2)\n        let oldI: number\n\n        // Searching for startIndex\n        do {\n          oldI = i\n          h = sizesValue[i].accumulator\n          if (h < scroll.start) {\n            a = i\n          }\n          else if (i < count - 1 && sizesValue[i + 1].accumulator > scroll.start) {\n            b = i\n          }\n          i = ~~((a + b) / 2)\n        } while (i !== oldI)\n        if (i < 0)\n          i = 0\n        startIndex = i\n\n        // For container style\n        totalSizeValue = sizesValue[count - 1].accumulator\n\n        // Searching for endIndex\n        for (endIndex = i; endIndex < count && sizesValue[endIndex].accumulator < scroll.end; endIndex++);\n        if (endIndex === -1) {\n          endIndex = items.length - 1\n        }\n        else {\n          endIndex++\n          // Bounds\n          if (endIndex > count)\n            endIndex = count\n        }\n\n        // search visible startIndex\n        for (visibleStartIndex = startIndex; visibleStartIndex < count && (beforeSize + sizesValue[visibleStartIndex].accumulator) < scroll.start; visibleStartIndex++);\n\n        // search visible endIndex\n        for (visibleEndIndex = visibleStartIndex; visibleEndIndex < count && (beforeSize + sizesValue[visibleEndIndex].accumulator) < scroll.end; visibleEndIndex++);\n      }\n      else {\n        // Fixed size mode\n        startIndex = ~~(scroll.start / itemSize * gridItems)\n        const remainer = startIndex % gridItems\n        startIndex -= remainer\n        endIndex = Math.ceil(scroll.end / itemSize * gridItems)\n        visibleStartIndex = Math.max(0, Math.floor((scroll.start - beforeSize) / itemSize * gridItems))\n        visibleEndIndex = Math.floor((scroll.end - beforeSize) / itemSize * gridItems)\n\n        // Bounds\n        if (startIndex < 0)\n          startIndex = 0\n        if (endIndex > count)\n          endIndex = count\n        if (visibleStartIndex < 0)\n          visibleStartIndex = 0\n        if (visibleEndIndex > count)\n          visibleEndIndex = count\n\n        totalSizeValue = Math.ceil(count / gridItems) * itemSize\n      }\n    }\n\n    if (endIndex - startIndex > config.itemsLimit) {\n      itemsLimitError()\n    }\n\n    totalSize.value = totalSizeValue\n\n    let view: View | undefined\n\n    const continuous = startIndex <= _endIndex && endIndex >= _startIndex\n\n    // Step 1: Mark any invisible elements as unused\n    if (!continuous || itemsChanged) {\n      removeAndRecycleAllViews()\n    }\n    else {\n      for (let i = 0, l = poolValue.length; i < l; i++) {\n        view = poolValue[i]\n        if (view.nr.used) {\n          const viewVisible = view.nr.index >= startIndex && view.nr.index < endIndex\n          const viewSize = itemSize || (sizesValue[i] && sizesValue[i].size)\n          if (!viewVisible || !viewSize) {\n            removeAndRecycleView(view)\n          }\n        }\n      }\n    }\n\n    // Step 2: Assign a view and update props for every view that became visible\n    let item: unknown, type: unknown\n    for (let i = startIndex; i < endIndex; i++) {\n      const elementSize = itemSize || (sizesValue[i] && sizesValue[i].size)\n      if (!elementSize)\n        continue\n      item = items[i]\n      const key = keyField ? (item as any)[keyField] : i\n      if (key == null) {\n        throw new Error(`Key is ${key} on item (keyField is '${keyField}')`)\n      }\n      view = views.get(key)\n\n      if (!view) {\n        // Item just became visible\n        type = (item as any)[typeField]\n        view = getRecycledView(type)\n\n        if (view) {\n          view.item = item\n          view.nr.index = i\n          view.nr.key = key\n          if (view.nr.type !== type) {\n            console.warn('Reused view\\'s type does not match pool\\'s type')\n          }\n        }\n        else {\n          // No recycled view available, create a new one\n          view = createView(poolValue, i, item, key, type)\n        }\n        views.set(key, view)\n      }\n      else {\n        if (view.item !== item) {\n          view.item = item\n        }\n        if (!view.nr.used) {\n          console.warn(`Expected existing view's used flag to be true, got ${view.nr.used}`)\n        }\n      }\n\n      // Update position\n      if (itemSize === null) {\n        view.position = sizesValue[i - 1].accumulator\n        view.offset = 0\n      }\n      else {\n        view.position = Math.floor(i / gridItems) * itemSize\n        view.offset = (i % gridItems) * itemSecondarySize\n      }\n    }\n\n    _startIndex = startIndex\n    _endIndex = endIndex\n\n    if (opts.emitUpdate)\n      callbacks?.onUpdate?.(startIndex, endIndex, visibleStartIndex, visibleEndIndex)\n\n    // After the user has finished scrolling\n    // Sort views so text selection is correct\n    if (_sortTimer)\n      clearTimeout(_sortTimer)\n    _sortTimer = setTimeout(sortViews, opts.updateInterval + 300)\n\n    return {\n      continuous,\n    }\n  }\n\n  function itemsLimitError() {\n    setTimeout(() => {\n      console.warn('It seems the scroller element isn\\'t scrolling, so it tries to render all the items at once.', 'Scroller:', toValue(el))\n      console.warn('Make sure the scroller has a fixed height (or width) and \\'overflow-y\\' (or \\'overflow-x\\') set to \\'auto\\' so it can scroll correctly and only render the items visible in the scroll viewport.')\n    })\n    throw new Error('Rendered items limit reached')\n  }\n\n  function hasVisibleViewGap(): boolean {\n    const visibleViews = pool.value.filter(({ nr }) => nr.used)\n    for (let i = 1; i < visibleViews.length; i++) {\n      if (visibleViews[i].nr.index !== visibleViews[i - 1].nr.index + 1) {\n        return true\n      }\n    }\n    return false\n  }\n\n  function sortViews() {\n    pool.value.sort((viewA, viewB) => viewA.nr.index - viewB.nr.index)\n\n    if (hasVisibleViewGap()) {\n      updateVisibleItems(false)\n      if (_sortTimer)\n        clearTimeout(_sortTimer)\n    }\n  }\n\n  function scrollToItem(index: number) {\n    const opts = toValue(options)\n    let scroll: number\n    const gridItems = opts.gridItems || 1\n    if (opts.itemSize === null) {\n      scroll = index > 0 ? (sizes.value as Sizes)[index - 1].accumulator : 0\n    }\n    else {\n      scroll = Math.floor(index / gridItems) * opts.itemSize\n    }\n    scrollToPosition(scroll)\n  }\n\n  function scrollToPosition(position: number) {\n    const opts = toValue(options)\n    const elValue = toValue(el)!\n    const direction = opts.direction === 'vertical'\n      ? { scroll: 'scrollTop' as const, start: 'top' as const }\n      : { scroll: 'scrollLeft' as const, start: 'left' as const }\n\n    if (opts.pageMode) {\n      const viewportEl = getScrollParent(elValue) as HTMLElement\n      // HTML doesn't overflow like other elements\n      const scrollTop = viewportEl.tagName === 'HTML' ? 0 : (viewportEl as any)[direction.scroll]\n      const bounds = viewportEl.getBoundingClientRect()\n\n      const scroller = elValue.getBoundingClientRect()\n      const scrollerPosition = scroller[direction.start] - bounds[direction.start]\n\n      ;(viewportEl as any)[direction.scroll] = position + scrollTop + scrollerPosition\n    }\n    else {\n      ;(elValue as any)[direction.scroll] = position\n    }\n  }\n\n  // In SSR mode, we also prerender the same number of item for the first render\n  const initialOpts = toValue(options)\n  if (initialOpts.prerender) {\n    _prerender = true\n    updateVisibleItems(false)\n  }\n\n  if (initialOpts.gridItems && !initialOpts.itemSize) {\n    console.error('[vue-recycle-scroller] You must provide an itemSize when using gridItems')\n  }\n\n  onMounted(() => {\n    applyPageMode()\n    nextTick(() => {\n      // In SSR mode, render the real number of visible items\n      _prerender = false\n      updateVisibleItems(true)\n      ready.value = true\n    })\n  })\n\n  onActivated(() => {\n    const lastPosition = _lastUpdateScrollPosition\n    if (typeof lastPosition === 'number') {\n      nextTick(() => {\n        scrollToPosition(lastPosition)\n      })\n    }\n  })\n\n  onBeforeUnmount(() => {\n    removeListeners()\n  })\n\n  // Watchers\n  watch(() => toValue(options).items, () => {\n    updateVisibleItems(true)\n  })\n\n  watch(() => toValue(options).pageMode, () => {\n    applyPageMode()\n    updateVisibleItems(false)\n  })\n\n  watch(sizes, () => {\n    updateVisibleItems(false)\n  }, { deep: true })\n\n  watch(() => toValue(options).gridItems, () => {\n    updateVisibleItems(true)\n  })\n\n  watch(() => toValue(options).itemSecondarySize, () => {\n    updateVisibleItems(true)\n  })\n\n  return {\n    pool,\n    totalSize,\n    ready,\n    sizes,\n    simpleArray,\n    scrollToItem,\n    scrollToPosition,\n    getScroll,\n    updateVisibleItems,\n    handleScroll,\n    handleResize,\n    handleVisibilityChange,\n    sortViews,\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/config.ts",
    "content": "export interface VirtualScrollerConfig {\n  itemsLimit: number\n  installComponents?: boolean\n  componentsPrefix?: string\n}\n\nconst config: VirtualScrollerConfig = {\n  itemsLimit: 1000,\n}\n\nexport default config\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/directives/observeVisibility.ts",
    "content": "import type { Directive, DirectiveBinding } from 'vue'\n\ntype ObserveVisibilityCallback = (isVisible: boolean, entry: IntersectionObserverEntry) => void\n\ninterface ObserveVisibilityValue {\n  callback: ObserveVisibilityCallback\n  intersection?: IntersectionObserverInit\n}\n\ninterface ObserveVisibilityState {\n  callback: ObserveVisibilityCallback\n  intersection?: IntersectionObserverInit\n  observer: IntersectionObserver | null\n  visible: boolean | null\n}\n\nconst stateMap = new WeakMap<Element, ObserveVisibilityState>()\n\nfunction normalizeValue(value: ObserveVisibilityCallback | ObserveVisibilityValue): ObserveVisibilityState {\n  if (typeof value === 'function') {\n    return {\n      callback: value,\n      observer: null,\n      intersection: undefined,\n      visible: null,\n    }\n  }\n\n  return {\n    callback: value.callback,\n    observer: null,\n    intersection: value.intersection,\n    visible: null,\n  }\n}\n\nfunction updateState(el: Element, binding: DirectiveBinding<ObserveVisibilityCallback | ObserveVisibilityValue>) {\n  teardown(el)\n  const state = normalizeValue(binding.value)\n  stateMap.set(el, state)\n\n  if (typeof IntersectionObserver === 'undefined') {\n    const rect = (el as HTMLElement).getBoundingClientRect()\n    state.visible = true\n    state.callback(true, {\n      boundingClientRect: rect,\n    } as IntersectionObserverEntry)\n    return\n  }\n\n  state.observer = new IntersectionObserver((entries) => {\n    const entry = entries[0]\n    const isVisible = !!entry?.isIntersecting\n    if (state.visible !== null && state.visible === isVisible)\n      return\n    state.visible = isVisible\n    state.callback(isVisible, entry)\n  }, state.intersection)\n\n  state.observer.observe(el)\n}\n\nfunction teardown(el: Element) {\n  const state = stateMap.get(el)\n  if (state?.observer) {\n    state.observer.disconnect()\n    state.observer = null\n  }\n}\n\nexport const ObserveVisibility: Directive<Element, ObserveVisibilityCallback | ObserveVisibilityValue> = {\n  mounted(el, binding) {\n    updateState(el, binding)\n  },\n  updated(el, binding) {\n    if (binding.value === binding.oldValue)\n      return\n    updateState(el, binding)\n  },\n  unmounted(el) {\n    teardown(el)\n    stateMap.delete(el)\n  },\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/index.spec.ts",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport config from './config'\nimport plugin, { DynamicScroller, DynamicScrollerItem, RecycleScroller } from './index'\n\nconst initialConfig = { ...config }\n\ndescribe('plugin', () => {\n  afterEach(() => {\n    Object.assign(config, initialConfig)\n  })\n\n  it('registers all components by default', () => {\n    const app = {\n      component: vi.fn(),\n    }\n\n    plugin.install(app as any)\n\n    expect(app.component).toHaveBeenCalledWith('recycle-scroller', RecycleScroller)\n    expect(app.component).toHaveBeenCalledWith('RecycleScroller', RecycleScroller)\n    expect(app.component).toHaveBeenCalledWith('dynamic-scroller', DynamicScroller)\n    expect(app.component).toHaveBeenCalledWith('DynamicScroller', DynamicScroller)\n    expect(app.component).toHaveBeenCalledWith('dynamic-scroller-item', DynamicScrollerItem)\n    expect(app.component).toHaveBeenCalledWith('DynamicScrollerItem', DynamicScrollerItem)\n  })\n\n  it('supports custom component prefixes', () => {\n    const app = {\n      component: vi.fn(),\n    }\n\n    plugin.install(app as any, {\n      componentsPrefix: 'V',\n    })\n\n    expect(app.component).toHaveBeenCalledWith('Vrecycle-scroller', RecycleScroller)\n    expect(app.component).toHaveBeenCalledWith('VRecycleScroller', RecycleScroller)\n    expect(app.component).toHaveBeenCalledWith('Vdynamic-scroller', DynamicScroller)\n    expect(app.component).toHaveBeenCalledWith('VDynamicScroller', DynamicScroller)\n    expect(app.component).toHaveBeenCalledWith('Vdynamic-scroller-item', DynamicScrollerItem)\n    expect(app.component).toHaveBeenCalledWith('VDynamicScrollerItem', DynamicScrollerItem)\n  })\n\n  it('does not register components when installComponents is false', () => {\n    const app = {\n      component: vi.fn(),\n    }\n\n    plugin.install(app as any, {\n      installComponents: false,\n      componentsPrefix: 'X',\n    })\n\n    expect(app.component).not.toHaveBeenCalled()\n    expect((config as any).installComponents).toBe(false)\n    expect((config as any).componentsPrefix).toBe('X')\n  })\n\n  it('exposes version', () => {\n    expect(typeof plugin.version).toBe('string')\n    expect(plugin.version.length).toBeGreaterThan(0)\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/index.ts",
    "content": "import type { App } from 'vue'\nimport type { PluginOptions } from './types'\nimport DynamicScroller from './components/DynamicScroller.vue'\nimport DynamicScrollerItem from './components/DynamicScrollerItem.vue'\nimport RecycleScroller from './components/RecycleScroller.vue'\nimport config from './config'\n\nexport { useDynamicScroller } from './composables/useDynamicScroller'\nexport type { UseDynamicScrollerOptions, UseDynamicScrollerReturn } from './composables/useDynamicScroller'\nexport { useDynamicScrollerItem } from './composables/useDynamicScrollerItem'\nexport type { UseDynamicScrollerItemOptions, UseDynamicScrollerItemReturn } from './composables/useDynamicScrollerItem'\n\nexport { useIdState } from './composables/useIdState'\nexport { useRecycleScroller } from './composables/useRecycleScroller'\nexport type { UseRecycleScrollerOptions, UseRecycleScrollerReturn } from './composables/useRecycleScroller'\n\nexport {\n  DynamicScroller,\n  DynamicScrollerItem,\n  RecycleScroller,\n}\n\nexport type * from './types'\n\nfunction registerComponents(app: App, prefix: string) {\n  app.component(`${prefix}recycle-scroller`, RecycleScroller)\n  app.component(`${prefix}RecycleScroller`, RecycleScroller)\n  app.component(`${prefix}dynamic-scroller`, DynamicScroller)\n  app.component(`${prefix}DynamicScroller`, DynamicScroller)\n  app.component(`${prefix}dynamic-scroller-item`, DynamicScrollerItem)\n  app.component(`${prefix}DynamicScrollerItem`, DynamicScrollerItem)\n}\n\ndeclare const VERSION: string\n\nconst plugin = {\n  version: VERSION,\n  install(app: App, options?: PluginOptions) {\n    const finalOptions = { ...{\n      installComponents: true,\n      componentsPrefix: '',\n    }, ...options }\n\n    for (const key in finalOptions) {\n      if (typeof (finalOptions as any)[key] !== 'undefined') {\n        (config as any)[key] = (finalOptions as any)[key]\n      }\n    }\n\n    if (finalOptions.installComponents) {\n      registerComponents(app, finalOptions.componentsPrefix!)\n    }\n  },\n}\n\nexport default plugin\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/scrollparent.spec.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { getScrollParent } from './scrollparent'\n\ndescribe('getScrollParent', () => {\n  it('returns the nearest scrollable parent', () => {\n    const outer = document.createElement('div')\n    outer.style.overflowY = 'auto'\n    const middle = document.createElement('div')\n    const inner = document.createElement('div')\n\n    middle.appendChild(inner)\n    outer.appendChild(middle)\n    document.body.appendChild(outer)\n\n    expect(getScrollParent(inner)).toBe(outer)\n\n    outer.remove()\n  })\n\n  it('falls back to scrollingElement when no scrollable parent exists', () => {\n    const inner = document.createElement('div')\n    document.body.appendChild(inner)\n\n    expect(getScrollParent(inner)).toBe(document.scrollingElement || document.documentElement)\n\n    inner.remove()\n  })\n\n  it('returns undefined for non-element nodes', () => {\n    expect(getScrollParent(document.createTextNode('x'))).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/scrollparent.ts",
    "content": "// Fork of https://github.com/olahol/scrollparent.js to be able to build with Rollup\n\nconst regex = /auto|scroll/\n\nfunction parents(node: Node, ps: Node[]): Node[] {\n  if (node.parentNode === null) {\n    return ps\n  }\n\n  return parents(node.parentNode, [...ps, ...[node]])\n}\n\nfunction style(node: Element, prop: string): string {\n  return getComputedStyle(node, null).getPropertyValue(prop)\n}\n\nfunction overflow(node: Element): string {\n  return style(node, 'overflow') + style(node, 'overflow-y') + style(node, 'overflow-x')\n}\n\nfunction scroll(node: Element): boolean {\n  return regex.test(overflow(node))\n}\n\nexport function getScrollParent(node: Node): Element | undefined {\n  if (!(node instanceof HTMLElement || node instanceof SVGElement)) {\n    return\n  }\n\n  const ps = parents(node.parentNode!, [])\n\n  for (let i = 0; i < ps.length; i += 1) {\n    if (ps[i] instanceof Element && scroll(ps[i] as Element)) {\n      return ps[i] as Element\n    }\n  }\n\n  return document.scrollingElement || document.documentElement\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/shims-vue.d.ts",
    "content": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n\n  const component: DefineComponent<object, object, any>\n  export default component\n}\n\ndeclare module 'mitt' {\n  type EventHandler = (event?: unknown) => void\n\n  interface Emitter {\n    all: Map<string, Set<EventHandler>>\n    on: (type: string, handler: EventHandler) => void\n    off: (type: string, handler: EventHandler) => void\n    emit: (type: string, event?: unknown) => void\n  }\n  export default function mitt(): Emitter\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/types.ts",
    "content": "export type ScrollDirection = 'vertical' | 'horizontal'\n\nexport interface ScrollState {\n  start: number\n  end: number\n}\n\nexport interface ViewNonReactive {\n  id: number\n  index: number\n  used: boolean\n  key: string | number\n  type: unknown\n}\n\nexport interface View {\n  item: unknown\n  position: number\n  offset: number\n  nr: ViewNonReactive\n}\n\nexport interface SizeEntry {\n  accumulator: number\n  size?: number\n}\n\nexport interface Sizes {\n  [key: number]: SizeEntry\n}\n\nexport interface VScrollData {\n  active: boolean\n  sizes: Record<string | number, number>\n  keyField: string\n  simpleArray: boolean\n}\n\nexport interface ItemWithSize {\n  item: unknown\n  id: string | number\n  size: number | undefined\n}\n\nexport interface PluginOptions {\n  installComponents?: boolean\n  componentsPrefix?: string\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/utils.spec.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { supportsPassive } from './utils'\n\ndescribe('supportsPassive', () => {\n  it('returns a boolean value', () => {\n    expect(typeof supportsPassive()).toBe('boolean')\n  })\n})\n"
  },
  {
    "path": "packages/vue-virtual-scroller/src/utils.ts",
    "content": "let _supportsPassive = false\n\nexport function supportsPassive(): boolean {\n  return _supportsPassive\n}\n\nif (typeof window !== 'undefined') {\n  _supportsPassive = false\n  try {\n    const opts = Object.defineProperty({}, 'passive', {\n      get() {\n        _supportsPassive = true\n      },\n    })\n    window.addEventListener('test', null as any, opts)\n  }\n  catch {\n    // noop\n  }\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\",\n    \"src/**/*.spec.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"jsx\": \"preserve\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationDir\": \"dist/types\",\n    \"noEmit\": true,\n    \"sourceMap\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.vue\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/vue-virtual-scroller/vite.config.ts",
    "content": "import { readFileSync } from 'node:fs'\nimport process from 'node:process'\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\nimport dts from 'vite-plugin-dts'\n\nconst pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))\nconst isTest = process.env.VITEST === 'true'\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    ...(!isTest\n      ? [dts({\n          tsconfigPath: './tsconfig.build.json',\n        })]\n      : []),\n  ],\n  define: {\n    VERSION: JSON.stringify(pkg.version),\n  },\n  test: {\n    environment: 'jsdom',\n    include: ['src/**/*.spec.ts'],\n  },\n  build: {\n    lib: {\n      entry: 'src/index.ts',\n      name: 'VueVirtualScroller',\n      formats: ['es'],\n      fileName: () => 'vue-virtual-scroller.js',\n    },\n    sourcemap: true,\n    rollupOptions: {\n      external: ['vue', 'mitt'],\n    },\n  },\n})\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "shellEmulator: true\n\ntrustPolicy: no-downgrade\n\npackages:\n  - packages/*\nonlyBuiltDependencies:\n  - esbuild\n"
  }
]