diff --git a/README.md b/README.md
index 3487ae4..82b149f 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,7 @@ The type signature of the options object:
 type Options = {
   vertical: boolean;
   horizontal: boolean;
+  debug: boolean;
 }
 ```
 
@@ -104,6 +105,10 @@ Type: `boolean`, default: `true`. Should the vertical scroll position be mirrore
 
 Type: `boolean`, default: `true`. Should the horizontal scroll position be mirrored?
 
+### `debug`
+
+Type: `boolean`, default: `true`. Should debug messages be printed to the console?
+
 ## API
 
 To access ScrollMirror's API, you have to save a reference to the class during instaciation:
@@ -114,14 +119,15 @@ const mirror = new ScrollMirror(document.querySelectorAll(".scroller"));
 
 ### `mirror.progress`
 
-Returns the current scroll progress in the form of `{x: number, y: number}`, where both x and y are a
+Get the current scroll progress in the form of `{ x: number, y: number }`, where both x and y are a
 number between 0-1
 
 ### `mirror.progress = value`
 
-Sets the progress and scrolls all mirrored elements. For example:
+Set the progress and scrolls all mirrored elements. For example:
 
 ```js
+// for both directions
 mirror.progress = { x: 0.2, y: 0.5 };
 // or only set one direction
 mirror.progress = { y: 0.5 };
@@ -131,7 +137,13 @@ mirror.progress = 0.5;
 
 ### `mirror.getScrollProgress(element: HTMLElement)`
 
-Return the current progress of an element. The element doesn't _need_ to be one of the mirrored elements
+Get the current progress of an element. The element doesn't _need_ to be one of the mirrored elements
+
+```ts
+const mirror = new ScrollMirror(document.querySelectorAll(".scroller"));
+// ...sometime later:
+console.log(mirror.getScrollProgress(document.querySelector(":root")));
+```
 
 ## Motivation
 
diff --git a/package.json b/package.json
index d8db793..fc6d668 100644
--- a/package.json
+++ b/package.json
@@ -16,15 +16,15 @@
   "license": "ISC",
   "type": "module",
   "source": "./src/index.ts",
-  "main": "./dist/index.cjs",
-  "module": "./dist/index.module.js",
-  "unpkg": "./dist/index.umd.js",
+  "main": "./dist/ScrollMirror.cjs",
+  "module": "./dist/ScrollMirror.module.js",
+  "unpkg": "./dist/ScrollMirror.umd.js",
   "types": "./dist/types/index.d.ts",
   "exports": {
     ".": {
       "types": "./dist/types/index.d.ts",
-      "import": "./dist/index.modern.js",
-      "require": "./dist/index.cjs"
+      "import": "./dist/ScrollMirror.modern.js",
+      "require": "./dist/ScrollMirror.cjs"
     }
   },
   "files": [
@@ -38,12 +38,14 @@
     "prepublishOnly": "pnpm run build",
     "build": "pnpm run clean && pnpm run build:module && pnpm run build:bundle",
     "build:module": "BROWSERSLIST_ENV=modern microbundle src/index.ts --format modern,esm,cjs",
-    "build:bundle": "BROWSERSLIST_ENV=production microbundle src/index.ts --format umd --external none",
+    "build:bundle": "BROWSERSLIST_ENV=production microbundle src/ScrollMirror.ts --format umd --external none",
     "watch": "BROWSERSLIST_ENV=development microbundle src/index.ts --watch --format modern",
     "docs:dev": "astro dev --root docs",
     "docs:build": "astro build --root docs",
     "docs:serve": "astro build --root docs && astro preview --root docs",
-    "test": "pnpm run test:e2e",
+    "test": "pnpm run test:unit && pnpm run test:e2e",
+    "test:unit": "vitest run --config ./tests/unit/vitest.config.ts",
+    "test:unit:watch": "vitest --config ./tests/unit/vitest.config.ts",
     "test:e2e": "pnpm exec playwright test --config ./tests/e2e/config.playwright.ts",
     "test:e2e:dev": "PLAYWRIGHT_ENV=dev pnpm run test:e2e --ui",
     "test:e2e:install": "pnpm exec playwright install --with-deps"
@@ -54,8 +56,10 @@
     "astro": "^4.15.9",
     "astro-expressive-code": "^0.37.0",
     "astro-feather": "^1.0.0",
+    "jsdom": "^25.0.1",
     "microbundle": "^0.15.1",
     "prettier": "^3.3.3",
-    "typescript": "^5.6.2"
+    "typescript": "^5.6.2",
+    "vitest": "^2.1.1"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e78f1a0..d3c7eaf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
       astro-feather:
         specifier: ^1.0.0
         version: 1.0.0
+      jsdom:
+        specifier: ^25.0.1
+        version: 25.0.1
       microbundle:
         specifier: ^0.15.1
         version: 0.15.1(@types/babel__core@7.20.5)
@@ -32,6 +35,9 @@ importers:
       typescript:
         specifier: ^5.6.2
         version: 5.6.2
+      vitest:
+        specifier: ^2.1.1
+        version: 2.1.1(@types/node@22.7.2)(jsdom@25.0.1)(terser@5.34.0)
 
 packages:
 
@@ -1221,6 +1227,36 @@ packages:
   '@ungap/structured-clone@1.2.0':
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
 
+  '@vitest/expect@2.1.1':
+    resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==}
+
+  '@vitest/mocker@2.1.1':
+    resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==}
+    peerDependencies:
+      '@vitest/spy': 2.1.1
+      msw: ^2.3.5
+      vite: ^5.0.0
+    peerDependenciesMeta:
+      msw:
+        optional: true
+      vite:
+        optional: true
+
+  '@vitest/pretty-format@2.1.1':
+    resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==}
+
+  '@vitest/runner@2.1.1':
+    resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==}
+
+  '@vitest/snapshot@2.1.1':
+    resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==}
+
+  '@vitest/spy@2.1.1':
+    resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==}
+
+  '@vitest/utils@2.1.1':
+    resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==}
+
   acorn-jsx@5.3.2:
     resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
     peerDependencies:
@@ -1231,6 +1267,10 @@ packages:
     engines: {node: '>=0.4.0'}
     hasBin: true
 
+  agent-base@7.1.1:
+    resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
+    engines: {node: '>= 14'}
+
   ansi-align@3.0.1:
     resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
 
@@ -1283,6 +1323,10 @@ packages:
     resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
     engines: {node: '>= 0.4'}
 
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
+
   astring@1.9.0:
     resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
     hasBin: true
@@ -1303,6 +1347,9 @@ packages:
   async@3.2.6:
     resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
 
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
   asyncro@3.0.0:
     resolution: {integrity: sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg==}
 
@@ -1393,6 +1440,10 @@ packages:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
 
+  cac@6.7.14:
+    resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+    engines: {node: '>=8'}
+
   call-bind@1.0.7:
     resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
     engines: {node: '>= 0.4'}
@@ -1418,6 +1469,10 @@ packages:
   ccount@2.0.1:
     resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
 
+  chai@5.1.1:
+    resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
+    engines: {node: '>=12'}
+
   chalk@1.1.3:
     resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
     engines: {node: '>=0.10.0'}
@@ -1446,6 +1501,10 @@ packages:
   character-reference-invalid@2.0.1:
     resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
 
+  check-error@2.1.1:
+    resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+    engines: {node: '>= 16'}
+
   ci-info@4.0.0:
     resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
     engines: {node: '>=8'}
@@ -1496,6 +1555,10 @@ packages:
   colord@2.9.3:
     resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
 
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
   comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
 
@@ -1579,6 +1642,14 @@ packages:
     resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==}
     engines: {node: '>=8.0.0'}
 
+  cssstyle@4.1.0:
+    resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==}
+    engines: {node: '>=18'}
+
+  data-urls@5.0.0:
+    resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+    engines: {node: '>=18'}
+
   data-view-buffer@1.0.1:
     resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
     engines: {node: '>= 0.4'}
@@ -1600,9 +1671,16 @@ packages:
       supports-color:
         optional: true
 
+  decimal.js@10.4.3:
+    resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
+
   decode-named-character-reference@1.0.2:
     resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
 
+  deep-eql@5.0.2:
+    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+    engines: {node: '>=6'}
+
   deepmerge@4.3.1:
     resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
     engines: {node: '>=0.10.0'}
@@ -1619,6 +1697,10 @@ packages:
     resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
     engines: {node: '>= 0.4'}
 
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
   dequal@2.0.3:
     resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
     engines: {node: '>=6'}
@@ -1845,6 +1927,10 @@ packages:
   for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
 
+  form-data@4.0.0:
+    resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
+    engines: {node: '>= 6'}
+
   fraction.js@4.3.7:
     resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
 
@@ -1890,6 +1976,9 @@ packages:
     resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
     engines: {node: '>=18'}
 
+  get-func-name@2.0.2:
+    resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+
   get-intrinsic@1.2.4:
     resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
     engines: {node: '>= 0.4'}
@@ -2023,6 +2112,10 @@ packages:
   hastscript@9.0.0:
     resolution: {integrity: sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==}
 
+  html-encoding-sniffer@4.0.0:
+    resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+    engines: {node: '>=18'}
+
   html-escaper@3.0.3:
     resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
 
@@ -2032,6 +2125,18 @@ packages:
   http-cache-semantics@4.1.1:
     resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
 
+  http-proxy-agent@7.0.2:
+    resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+    engines: {node: '>= 14'}
+
+  https-proxy-agent@7.0.5:
+    resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
+    engines: {node: '>= 14'}
+
+  iconv-lite@0.6.3:
+    resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+    engines: {node: '>=0.10.0'}
+
   icss-replace-symbols@1.1.0:
     resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==}
 
@@ -2172,6 +2277,9 @@ packages:
     resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
     engines: {node: '>=12'}
 
+  is-potential-custom-element-name@1.0.1:
+    resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
   is-reference@1.2.1:
     resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
 
@@ -2240,6 +2348,15 @@ packages:
     resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
     hasBin: true
 
+  jsdom@25.0.1:
+    resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      canvas: ^2.11.2
+    peerDependenciesMeta:
+      canvas:
+        optional: true
+
   jsesc@0.5.0:
     resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
     hasBin: true
@@ -2313,6 +2430,9 @@ packages:
   longest-streak@3.1.0:
     resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
 
+  loupe@3.1.1:
+    resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
+
   lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
 
@@ -2514,6 +2634,14 @@ packages:
     resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
     engines: {node: '>=8.6'}
 
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
   mimic-function@5.0.1:
     resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
     engines: {node: '>=18'}
@@ -2569,6 +2697,9 @@ packages:
     resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==}
     engines: {node: '>=0.10.0'}
 
+  nwsapi@2.2.13:
+    resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==}
+
   object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
@@ -2671,6 +2802,13 @@ packages:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
 
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
+  pathval@2.0.0:
+    resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
+    engines: {node: '>= 14.16'}
+
   periscopic@3.1.0:
     resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
 
@@ -2958,6 +3096,10 @@ packages:
   property-information@6.5.0:
     resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
 
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
   queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
@@ -3106,6 +3248,9 @@ packages:
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
+  rrweb-cssom@0.7.1:
+    resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+
   run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 
@@ -3127,6 +3272,13 @@ packages:
     resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
     engines: {node: '>= 0.4'}
 
+  safer-buffer@2.1.2:
+    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+  saxes@6.0.0:
+    resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+    engines: {node: '>=v12.22.7'}
+
   section-matter@1.0.0:
     resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
     engines: {node: '>=4'}
@@ -3162,6 +3314,9 @@ packages:
     resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
     engines: {node: '>= 0.4'}
 
+  siginfo@2.0.0:
+    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
   signal-exit@4.1.0:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     engines: {node: '>=14'}
@@ -3205,6 +3360,12 @@ packages:
     resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
     deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
 
+  stackback@0.0.2:
+    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+  std-env@3.7.0:
+    resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
+
   stdin-discarder@0.2.2:
     resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
     engines: {node: '>=18'}
@@ -3298,6 +3459,9 @@ packages:
     engines: {node: '>=10.13.0'}
     hasBin: true
 
+  symbol-tree@3.2.4:
+    resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
   terser@5.34.0:
     resolution: {integrity: sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==}
     engines: {node: '>=10'}
@@ -3306,9 +3470,31 @@ packages:
   tiny-glob@0.2.9:
     resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
 
+  tinybench@2.9.0:
+    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
   tinyexec@0.3.0:
     resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==}
 
+  tinypool@1.0.1:
+    resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+    engines: {node: '>=14.0.0'}
+
+  tinyspy@3.0.2:
+    resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+    engines: {node: '>=14.0.0'}
+
+  tldts-core@6.1.48:
+    resolution: {integrity: sha512-3gD9iKn/n2UuFH1uilBviK9gvTNT6iYwdqrj1Vr5mh8FuelvpRNaYVH4pNYqUgOGU4aAdL9X35eLuuj0gRsx+A==}
+
+  tldts@6.1.48:
+    resolution: {integrity: sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==}
+    hasBin: true
+
   to-fast-properties@2.0.0:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}
@@ -3317,6 +3503,14 @@ packages:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
 
+  tough-cookie@5.0.0:
+    resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==}
+    engines: {node: '>=16'}
+
+  tr46@5.0.0:
+    resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
+    engines: {node: '>=18'}
+
   trim-lines@3.0.1:
     resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
 
@@ -3443,6 +3637,11 @@ packages:
   vfile@6.0.3:
     resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
 
+  vite-node@2.1.1:
+    resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+
   vite@5.4.8:
     resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -3482,9 +3681,54 @@ packages:
       vite:
         optional: true
 
+  vitest@2.1.1:
+    resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@types/node': ^18.0.0 || >=20.0.0
+      '@vitest/browser': 2.1.1
+      '@vitest/ui': 2.1.1
+      happy-dom: '*'
+      jsdom: '*'
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+
+  w3c-xmlserializer@5.0.0:
+    resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+    engines: {node: '>=18'}
+
   web-namespaces@2.0.1:
     resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
 
+  webidl-conversions@7.0.0:
+    resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+    engines: {node: '>=12'}
+
+  whatwg-encoding@3.1.1:
+    resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+    engines: {node: '>=18'}
+
+  whatwg-mimetype@4.0.0:
+    resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+    engines: {node: '>=18'}
+
+  whatwg-url@14.0.0:
+    resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
+    engines: {node: '>=18'}
+
   which-boxed-primitive@1.0.2:
     resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
 
@@ -3500,6 +3744,11 @@ packages:
     resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
     engines: {node: '>= 0.4'}
 
+  why-is-node-running@2.3.0:
+    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+    engines: {node: '>=8'}
+    hasBin: true
+
   widest-line@4.0.1:
     resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
     engines: {node: '>=12'}
@@ -3515,6 +3764,25 @@ packages:
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
+  ws@8.18.0:
+    resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  xml-name-validator@5.0.0:
+    resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+    engines: {node: '>=18'}
+
+  xmlchars@2.2.0:
+    resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
   xxhash-wasm@1.0.2:
     resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==}
 
@@ -4924,12 +5192,58 @@ snapshots:
 
   '@ungap/structured-clone@1.2.0': {}
 
+  '@vitest/expect@2.1.1':
+    dependencies:
+      '@vitest/spy': 2.1.1
+      '@vitest/utils': 2.1.1
+      chai: 5.1.1
+      tinyrainbow: 1.2.0
+
+  '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.7.2)(terser@5.34.0))':
+    dependencies:
+      '@vitest/spy': 2.1.1
+      estree-walker: 3.0.3
+      magic-string: 0.30.11
+    optionalDependencies:
+      vite: 5.4.8(@types/node@22.7.2)(terser@5.34.0)
+
+  '@vitest/pretty-format@2.1.1':
+    dependencies:
+      tinyrainbow: 1.2.0
+
+  '@vitest/runner@2.1.1':
+    dependencies:
+      '@vitest/utils': 2.1.1
+      pathe: 1.1.2
+
+  '@vitest/snapshot@2.1.1':
+    dependencies:
+      '@vitest/pretty-format': 2.1.1
+      magic-string: 0.30.11
+      pathe: 1.1.2
+
+  '@vitest/spy@2.1.1':
+    dependencies:
+      tinyspy: 3.0.2
+
+  '@vitest/utils@2.1.1':
+    dependencies:
+      '@vitest/pretty-format': 2.1.1
+      loupe: 3.1.1
+      tinyrainbow: 1.2.0
+
   acorn-jsx@5.3.2(acorn@8.12.1):
     dependencies:
       acorn: 8.12.1
 
   acorn@8.12.1: {}
 
+  agent-base@7.1.1:
+    dependencies:
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+
   ansi-align@3.0.1:
     dependencies:
       string-width: 4.2.3
@@ -4978,6 +5292,8 @@ snapshots:
       is-array-buffer: 3.0.4
       is-shared-array-buffer: 1.0.3
 
+  assertion-error@2.0.1: {}
+
   astring@1.9.0: {}
 
   astro-expressive-code@0.37.0(astro@4.15.9(@types/node@22.7.2)(rollup@2.79.1)(terser@5.34.0)(typescript@5.6.2)):
@@ -5071,6 +5387,8 @@ snapshots:
 
   async@3.2.6: {}
 
+  asynckit@0.4.0: {}
+
   asyncro@3.0.0: {}
 
   autoprefixer@10.4.20(postcss@8.4.47):
@@ -5175,6 +5493,8 @@ snapshots:
 
   builtin-modules@3.3.0: {}
 
+  cac@6.7.14: {}
+
   call-bind@1.0.7:
     dependencies:
       es-define-property: 1.0.0
@@ -5200,6 +5520,14 @@ snapshots:
 
   ccount@2.0.1: {}
 
+  chai@5.1.1:
+    dependencies:
+      assertion-error: 2.0.1
+      check-error: 2.1.1
+      deep-eql: 5.0.2
+      loupe: 3.1.1
+      pathval: 2.0.0
+
   chalk@1.1.3:
     dependencies:
       ansi-styles: 2.2.1
@@ -5229,6 +5557,8 @@ snapshots:
 
   character-reference-invalid@2.0.1: {}
 
+  check-error@2.1.1: {}
+
   ci-info@4.0.0: {}
 
   cli-boxes@3.0.0: {}
@@ -5275,6 +5605,10 @@ snapshots:
 
   colord@2.9.3: {}
 
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
   comma-separated-tokens@2.0.3: {}
 
   commander@2.20.3: {}
@@ -5378,6 +5712,15 @@ snapshots:
     dependencies:
       css-tree: 1.1.3
 
+  cssstyle@4.1.0:
+    dependencies:
+      rrweb-cssom: 0.7.1
+
+  data-urls@5.0.0:
+    dependencies:
+      whatwg-mimetype: 4.0.0
+      whatwg-url: 14.0.0
+
   data-view-buffer@1.0.1:
     dependencies:
       call-bind: 1.0.7
@@ -5400,10 +5743,14 @@ snapshots:
     dependencies:
       ms: 2.1.3
 
+  decimal.js@10.4.3: {}
+
   decode-named-character-reference@1.0.2:
     dependencies:
       character-entities: 2.0.2
 
+  deep-eql@5.0.2: {}
+
   deepmerge@4.3.1: {}
 
   define-data-property@1.1.4:
@@ -5420,6 +5767,8 @@ snapshots:
       has-property-descriptors: 1.0.2
       object-keys: 1.1.1
 
+  delayed-stream@1.0.0: {}
+
   dequal@2.0.3: {}
 
   detect-libc@2.0.3:
@@ -5700,6 +6049,12 @@ snapshots:
     dependencies:
       is-callable: 1.2.7
 
+  form-data@4.0.0:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+
   fraction.js@4.3.7: {}
 
   fs-extra@10.1.0:
@@ -5737,6 +6092,8 @@ snapshots:
 
   get-east-asian-width@1.2.0: {}
 
+  get-func-name@2.0.2: {}
+
   get-intrinsic@1.2.4:
     dependencies:
       es-errors: 1.3.0
@@ -5987,12 +6344,34 @@ snapshots:
       property-information: 6.5.0
       space-separated-tokens: 2.0.2
 
+  html-encoding-sniffer@4.0.0:
+    dependencies:
+      whatwg-encoding: 3.1.1
+
   html-escaper@3.0.3: {}
 
   html-void-elements@3.0.0: {}
 
   http-cache-semantics@4.1.1: {}
 
+  http-proxy-agent@7.0.2:
+    dependencies:
+      agent-base: 7.1.1
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.5:
+    dependencies:
+      agent-base: 7.1.1
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+
+  iconv-lite@0.6.3:
+    dependencies:
+      safer-buffer: 2.1.2
+
   icss-replace-symbols@1.1.0: {}
 
   icss-utils@5.1.0(postcss@8.4.47):
@@ -6107,6 +6486,8 @@ snapshots:
 
   is-plain-obj@4.1.0: {}
 
+  is-potential-custom-element-name@1.0.1: {}
+
   is-reference@1.2.1:
     dependencies:
       '@types/estree': 1.0.6
@@ -6178,6 +6559,34 @@ snapshots:
     dependencies:
       argparse: 2.0.1
 
+  jsdom@25.0.1:
+    dependencies:
+      cssstyle: 4.1.0
+      data-urls: 5.0.0
+      decimal.js: 10.4.3
+      form-data: 4.0.0
+      html-encoding-sniffer: 4.0.0
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.5
+      is-potential-custom-element-name: 1.0.1
+      nwsapi: 2.2.13
+      parse5: 7.1.2
+      rrweb-cssom: 0.7.1
+      saxes: 6.0.0
+      symbol-tree: 3.2.4
+      tough-cookie: 5.0.0
+      w3c-xmlserializer: 5.0.0
+      webidl-conversions: 7.0.0
+      whatwg-encoding: 3.1.1
+      whatwg-mimetype: 4.0.0
+      whatwg-url: 14.0.0
+      ws: 8.18.0
+      xml-name-validator: 5.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+
   jsesc@0.5.0: {}
 
   jsesc@2.5.2: {}
@@ -6232,6 +6641,10 @@ snapshots:
 
   longest-streak@3.1.0: {}
 
+  loupe@3.1.1:
+    dependencies:
+      get-func-name: 2.0.2
+
   lru-cache@5.1.1:
     dependencies:
       yallist: 3.1.1
@@ -6759,6 +7172,12 @@ snapshots:
       braces: 3.0.3
       picomatch: 2.3.1
 
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
   mimic-function@5.0.1: {}
 
   minimatch@3.1.2:
@@ -6797,6 +7216,8 @@ snapshots:
 
   number-is-nan@1.0.1: {}
 
+  nwsapi@2.2.13: {}
+
   object-assign@4.1.1: {}
 
   object-inspect@1.13.2: {}
@@ -6915,6 +7336,10 @@ snapshots:
 
   path-type@4.0.0: {}
 
+  pathe@1.1.2: {}
+
+  pathval@2.0.0: {}
+
   periscopic@3.1.0:
     dependencies:
       '@types/estree': 1.0.6
@@ -7174,6 +7599,8 @@ snapshots:
 
   property-information@6.5.0: {}
 
+  punycode@2.3.1: {}
+
   queue-microtask@1.2.3: {}
 
   randombytes@2.1.0:
@@ -7416,6 +7843,8 @@ snapshots:
       '@rollup/rollup-win32-x64-msvc': 4.22.4
       fsevents: 2.3.3
 
+  rrweb-cssom@0.7.1: {}
+
   run-parallel@1.2.0:
     dependencies:
       queue-microtask: 1.2.3
@@ -7441,6 +7870,12 @@ snapshots:
       es-errors: 1.3.0
       is-regex: 1.1.4
 
+  safer-buffer@2.1.2: {}
+
+  saxes@6.0.0:
+    dependencies:
+      xmlchars: 2.2.0
+
   section-matter@1.0.0:
     dependencies:
       extend-shallow: 2.0.1
@@ -7513,6 +7948,8 @@ snapshots:
       get-intrinsic: 1.2.4
       object-inspect: 1.13.2
 
+  siginfo@2.0.0: {}
+
   signal-exit@4.1.0: {}
 
   simple-swizzle@0.2.2:
@@ -7543,6 +7980,10 @@ snapshots:
 
   stable@0.1.8: {}
 
+  stackback@0.0.2: {}
+
+  std-env@3.7.0: {}
+
   stdin-discarder@0.2.2: {}
 
   string-hash@1.1.3: {}
@@ -7658,6 +8099,8 @@ snapshots:
       picocolors: 1.1.0
       stable: 0.1.8
 
+  symbol-tree@3.2.4: {}
+
   terser@5.34.0:
     dependencies:
       '@jridgewell/source-map': 0.3.6
@@ -7670,14 +8113,36 @@ snapshots:
       globalyzer: 0.1.0
       globrex: 0.1.2
 
+  tinybench@2.9.0: {}
+
   tinyexec@0.3.0: {}
 
+  tinypool@1.0.1: {}
+
+  tinyrainbow@1.2.0: {}
+
+  tinyspy@3.0.2: {}
+
+  tldts-core@6.1.48: {}
+
+  tldts@6.1.48:
+    dependencies:
+      tldts-core: 6.1.48
+
   to-fast-properties@2.0.0: {}
 
   to-regex-range@5.0.1:
     dependencies:
       is-number: 7.0.0
 
+  tough-cookie@5.0.0:
+    dependencies:
+      tldts: 6.1.48
+
+  tr46@5.0.0:
+    dependencies:
+      punycode: 2.3.1
+
   trim-lines@3.0.1: {}
 
   trough@2.2.0: {}
@@ -7827,6 +8292,23 @@ snapshots:
       '@types/unist': 3.0.3
       vfile-message: 4.0.2
 
+  vite-node@2.1.1(@types/node@22.7.2)(terser@5.34.0):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.3.7
+      pathe: 1.1.2
+      vite: 5.4.8(@types/node@22.7.2)(terser@5.34.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
   vite@5.4.8(@types/node@22.7.2)(terser@5.34.0):
     dependencies:
       esbuild: 0.21.5
@@ -7841,8 +8323,60 @@ snapshots:
     optionalDependencies:
       vite: 5.4.8(@types/node@22.7.2)(terser@5.34.0)
 
+  vitest@2.1.1(@types/node@22.7.2)(jsdom@25.0.1)(terser@5.34.0):
+    dependencies:
+      '@vitest/expect': 2.1.1
+      '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.7.2)(terser@5.34.0))
+      '@vitest/pretty-format': 2.1.1
+      '@vitest/runner': 2.1.1
+      '@vitest/snapshot': 2.1.1
+      '@vitest/spy': 2.1.1
+      '@vitest/utils': 2.1.1
+      chai: 5.1.1
+      debug: 4.3.7
+      magic-string: 0.30.11
+      pathe: 1.1.2
+      std-env: 3.7.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.0
+      tinypool: 1.0.1
+      tinyrainbow: 1.2.0
+      vite: 5.4.8(@types/node@22.7.2)(terser@5.34.0)
+      vite-node: 2.1.1(@types/node@22.7.2)(terser@5.34.0)
+      why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/node': 22.7.2
+      jsdom: 25.0.1
+    transitivePeerDependencies:
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  w3c-xmlserializer@5.0.0:
+    dependencies:
+      xml-name-validator: 5.0.0
+
   web-namespaces@2.0.1: {}
 
+  webidl-conversions@7.0.0: {}
+
+  whatwg-encoding@3.1.1:
+    dependencies:
+      iconv-lite: 0.6.3
+
+  whatwg-mimetype@4.0.0: {}
+
+  whatwg-url@14.0.0:
+    dependencies:
+      tr46: 5.0.0
+      webidl-conversions: 7.0.0
+
   which-boxed-primitive@1.0.2:
     dependencies:
       is-bigint: 1.0.4
@@ -7865,6 +8399,11 @@ snapshots:
       gopd: 1.0.1
       has-tostringtag: 1.0.2
 
+  why-is-node-running@2.3.0:
+    dependencies:
+      siginfo: 2.0.0
+      stackback: 0.0.2
+
   widest-line@4.0.1:
     dependencies:
       string-width: 5.1.2
@@ -7883,6 +8422,12 @@ snapshots:
 
   wrappy@1.0.2: {}
 
+  ws@8.18.0: {}
+
+  xml-name-validator@5.0.0: {}
+
+  xmlchars@2.2.0: {}
+
   xxhash-wasm@1.0.2: {}
 
   y18n@5.0.8: {}
diff --git a/src/ScrollMirror.ts b/src/ScrollMirror.ts
new file mode 100644
index 0000000..d25262e
--- /dev/null
+++ b/src/ScrollMirror.ts
@@ -0,0 +1,202 @@
+import type { Progress, Options, Logger } from "./support/defs.js";
+import {
+  getScrollProgress,
+  hasOverflow,
+  nextTick,
+} from "./support/helpers.js";
+
+import {
+  getScrollEventTarget,
+  getLogger,
+  validateElements,
+  validateProgress,
+} from "./support/functions.js";
+
+/**
+ * Mirrors the scroll position of multiple elements on a page
+ */
+export default class ScrollMirror {
+  /** Mirror the scroll positions of these elements */
+  readonly elements: HTMLElement[];
+  /** The default options */
+  readonly defaults: Options = {
+    vertical: true,
+    horizontal: true,
+    debug: true
+  };
+  /** The parsed options */
+  options: Options;
+  /** Is mirroring paused? */
+  paused: boolean = false;
+  /** a simple logger @internal */
+  logger: Logger | undefined = undefined;
+
+  constructor(
+    elements: NodeListOf<Element> | Element[],
+    options: Partial<Options> = {}
+  ) {
+    this.elements = [...elements]
+      .filter(Boolean)
+      .map((el) => this.getScrollContainer(el));
+
+    /** remove duplicates */
+    this.elements = [...new Set(this.elements)];
+
+    this.options = { ...this.defaults, ...options };
+
+    if (this.options.debug) {
+      this.logger = getLogger("[scroll-mirror]");
+      validateElements(this.elements, this.logger);
+    }
+
+    this.elements.forEach((element) => this.addScrollHandler(element));
+
+    /**
+     * Initially, make sure that elements are mirrored to the
+     * documentElement's scroll position (if provided)
+     */
+    if (this.elements.includes(document.documentElement)) {
+      this.mirrorScrollPositions(
+        getScrollProgress(document.documentElement),
+        document.documentElement
+      );
+    }
+  }
+
+  /** Pause mirroring */
+  pause() {
+    this.paused = true;
+  }
+
+  /** Resume mirroring */
+  resume() {
+    this.paused = false;
+  }
+
+  /** Destroy. Removes all event handlers */
+  destroy() {
+    this.elements.forEach((element) => this.removeScrollHandler(element));
+  }
+
+  /** Add the scroll handler to the element @internal */
+  addScrollHandler(element: HTMLElement) {
+    /** Safeguard to prevent duplicate handlers on elements */
+    this.removeScrollHandler(element);
+
+    const target = getScrollEventTarget(element);
+    target.addEventListener("scroll", this.handleScroll);
+  }
+
+  /** Remove the scroll handler from an element @internal */
+  removeScrollHandler(element: HTMLElement) {
+    const target = getScrollEventTarget(element);
+    target.removeEventListener("scroll", this.handleScroll);
+  }
+
+  /**
+   * Get the scroll container, based on element provided:
+   * - return the element if it's a child of <body>
+   * - otherwise, return the documentElement
+   */
+  getScrollContainer(el: unknown): HTMLElement {
+    if (el instanceof HTMLElement && el.matches("body *")) return el;
+    return document.documentElement;
+  }
+
+  /** Handle a scroll event on an element @internal */
+  handleScroll = async (event: Event) => {
+    if (this.paused) return;
+
+    if (!event.currentTarget) return;
+
+    const scrolledElement = this.getScrollContainer(event.currentTarget);
+
+    await nextTick();
+
+    this.mirrorScrollPositions(
+      getScrollProgress(scrolledElement),
+      scrolledElement
+    );
+  };
+
+  /** Mirror the scroll positions of all elements to a target @internal */
+  mirrorScrollPositions(
+    progress: Progress,
+    ignore: HTMLElement | undefined = undefined
+  ) {
+    this.elements.forEach((element) => {
+      /* Ignore the currently scrolled element  */
+      if (ignore === element) return;
+
+      /* Remove the scroll event listener */
+      this.removeScrollHandler(element);
+
+      this.setScrollPosition(progress, element);
+
+      /* Re-attach the scroll event listener */
+      window.requestAnimationFrame(() => {
+        this.addScrollHandler(element);
+      });
+    });
+  }
+
+  /** Mirror the scroll position from one element to another @internal */
+  setScrollPosition(progress: Progress, target: HTMLElement) {
+    const { vertical, horizontal } = this.options;
+
+    /* Calculate the actual element scroll lengths */
+    const availableScroll = {
+      x: target.scrollWidth - target.clientWidth,
+      y: target.scrollHeight - target.clientHeight,
+    };
+
+    /* Adjust the scroll position accordingly */
+    if (vertical && !!availableScroll.y) {
+      target.scrollTo({
+        top: availableScroll.y * progress.y,
+        behavior: "instant",
+      });
+    }
+    if (horizontal && !!availableScroll.x) {
+      target.scrollTo({
+        left: availableScroll.x * progress.x,
+        behavior: "instant",
+      });
+    }
+  }
+
+  /**
+   * Get the scroll position from the first container that has overflow
+   */
+  get progress(): Progress {
+    const firstWithOverflow = this.elements.find((el) => hasOverflow(el));
+
+    return getScrollProgress(firstWithOverflow);
+  }
+
+  /**
+   * Set the scroll progress of all mirrored elements
+   *
+   * The progress is an object of { x:number , y: number }, where both x and y are a number
+   * between 0-1
+   *
+   * Examples:
+   *  - `const progress = mirror.progress` — returns something like { x: 0.2, y:0.5 }
+   *  - `mirror.progress = 0.5` — set the scroll position to 50% on both axes
+   *  - `mirror.progress = { y: 0.5 }` — set the scroll position to 50% on the y axis
+   *  - `mirror.progress = { x: 0.2, y: 0.5 }` — set the scroll position on both axes
+   */
+  set progress(value: Partial<Progress> | number) {
+    /** if the value is a number, set both axes to that value */
+    if (typeof value === "number") {
+      value = { x: value, y: value };
+    }
+    const progress = { ...this.progress, ...value };
+
+    if (!validateProgress(progress, this.logger)) {
+      return;
+    }
+
+    this.mirrorScrollPositions(progress);
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index e01e825..0bb2982 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,262 +1,12 @@
-import { hasCSSOverflow, hasOverflow, nextTick } from "./support/utils.js";
-
-export type Options = {
-  /** Mirror the vertical scroll position */
-  vertical: boolean;
-  /** Mirror the horizontal scroll position */
-  horizontal: boolean;
-};
-
-export type Progress = {
-  x: number;
-  y: number;
-};
-
-/**
- * Mirrors the scroll position of multiple elements on a page
- */
-export default class ScrollMirror {
-  /** Mirror the scroll positions of these elements */
-  readonly elements: HTMLElement[];
-  /** The default options */
-  readonly defaults: Options = {
-    vertical: true,
-    horizontal: true,
-  };
-  /** The parsed options */
-  options: Options;
-  /** Is mirroring paused? */
-  paused: boolean = false;
-  /** @internal */
-  prefix: string = "[scroll-mirror]";
-
-  constructor(
-    elements: NodeListOf<Element> | Element[],
-    options: Partial<Options> = {},
-  ) {
-    this.elements = [...elements]
-      .filter(Boolean)
-      .map((el) => this.getScrollContainer(el));
-
-    // remove duplicates
-    this.elements = [...new Set(this.elements)];
-
-    this.options = { ...this.defaults, ...options };
-
-    if (!this.validateElements()) return;
-
-    this.elements.forEach((element) => this.addHandler(element));
-    /**
-     * Initially, make sure that elements are mirrored to the
-     * documentElement's scroll position (if provided)
-     */
-    if (this.elements.includes(document.documentElement)) {
-      this.mirrorScrollPositions(
-        this.getScrollProgress(document.documentElement),
-        document.documentElement,
-      );
-    }
-  }
-
-  /** Pause mirroring */
-  pause() {
-    this.paused = true;
-  }
-
-  /** Resume mirroring */
-  resume() {
-    this.paused = false;
-  }
-
-  /** Destroy. Removes all event handlers */
-  destroy() {
-    this.elements.forEach((element) => this.removeHandler(element));
-  }
-
-  /** Make sure the provided elements are valid @internal */
-  validateElements(): boolean {
-    const elements = [...this.elements];
-    if (elements.length < 2) {
-      console.error(`${this.prefix} Please provide at least two elements`);
-      return false;
-    }
-    for (const element of elements) {
-      if (!element) {
-        console.warn(`${this.prefix} element is not defined:`, element);
-        return false;
-      }
-      if (element instanceof HTMLElement && !hasOverflow(element)) {
-        console.warn(`${this.prefix} element doesn't have overflow:`, element);
-      }
-      if (
-        element instanceof HTMLElement &&
-        element.matches("body *") &&
-        !hasCSSOverflow(element)
-      ) {
-        console.warn(
-          `${this.prefix} no "overflow: auto;" or "overflow: scroll;" set on element:`,
-          element,
-        );
-      }
-    }
-    return true;
-  }
-
-  /** Add the scroll handler to the element @internal */
-  addHandler(element: HTMLElement) {
-    /** Safeguard to prevent duplicate handlers on elements */
-    this.removeHandler(element);
-
-    const target = this.getEventTarget(element);
-    target.addEventListener("scroll", this.handleScroll);
-  }
-
-  /** Remove the scroll handler from an element @internal */
-  removeHandler(element: HTMLElement) {
-    const target = this.getEventTarget(element);
-    target.removeEventListener("scroll", this.handleScroll);
-  }
-
-  /**
-   * Get the scroll container, based on element provided:
-   * - return the element if it's a child of <body>
-   * - otherwise, return the documentElement
-   */
-  getScrollContainer(el: unknown): HTMLElement {
-    if (el instanceof HTMLElement && el.matches("body *")) return el;
-    return document.documentElement;
-  }
-
-  /**
-   * Get the event target for receiving scroll events
-   * - return the window if the element is either the html or body element
-   * - otherwise, return the element
-   */
-  getEventTarget(element: HTMLElement): Window | HTMLElement {
-    return element.matches("body *") ? element : window;
-  }
-
-  /** Handle a scroll event on an element @internal */
-  handleScroll = async (event: Event) => {
-    if (this.paused) return;
-
-    if (!event.currentTarget) return;
-
-    const scrolledElement = this.getScrollContainer(event.currentTarget);
-
-    await nextTick();
-
-    this.mirrorScrollPositions(
-      this.getScrollProgress(scrolledElement),
-      scrolledElement,
-    );
-  };
-
-  /** Mirror the scroll positions of all elements to a target @internal */
-  mirrorScrollPositions(
-    progress: Progress,
-    ignore: HTMLElement | undefined = undefined,
-  ) {
-    this.elements.forEach((element) => {
-      /* Ignore the currently scrolled element  */
-      if (ignore === element) return;
-
-      /* Remove the scroll event listener */
-      this.removeHandler(element);
-
-      this.setScrollPosition(progress, element);
-
-      /* Re-attach the scroll event listener */
-      window.requestAnimationFrame(() => {
-        this.addHandler(element);
-      });
-    });
-  }
-
-  /** Mirror the scroll position from one element to another @internal */
-  setScrollPosition(progress: Progress, target: HTMLElement) {
-    const { vertical, horizontal } = this.options;
-
-    /* Calculate the actual element scroll lengths */
-    const availableScroll = {
-      x: target.scrollWidth - target.clientWidth,
-      y: target.scrollHeight - target.clientHeight,
-    };
-
-    /* Adjust the scroll position accordingly */
-    if (vertical && !!availableScroll.y) {
-      target.scrollTo({
-        top: availableScroll.y * progress.y,
-        behavior: "instant",
-      });
-    }
-    if (horizontal && !!availableScroll.x) {
-      target.scrollTo({
-        left: availableScroll.x * progress.x,
-        behavior: "instant",
-      });
-    }
-  }
-
-  /** Get the scroll progress of an element, between 0-1 */
-  getScrollProgress(el: HTMLElement): Progress {
-    const {
-      scrollTop,
-      scrollHeight,
-      clientHeight,
-      scrollLeft,
-      scrollWidth,
-      clientWidth,
-    } = el;
-
-    const availableWidth = scrollWidth - clientWidth;
-    const availableHeight = scrollHeight - clientHeight;
-
-    return {
-      x: !!scrollLeft ? scrollLeft / Math.max(0.00001, availableWidth) : 0,
-      y: !!scrollTop ? scrollTop / Math.max(0.00001, availableHeight) : 0,
-    };
-  }
-
-  get progress(): Progress {
-    return this.getScrollProgress(this.elements[0]);
-  }
-
-  /**
-   * Get or set the scroll progress of all mirrored elements
-   *
-   * The progress is an object of { x:number , y: number }, where both x and y are a number
-   * between 0-1
-   *
-   * Examples:
-   *  - `const progress = mirror.progress` — returns something like { x: 0.2, y:0.5 }
-   *  - `mirror.progress = 0.5` — set the scroll position to 50% on both axes
-   *  - `mirror.progress = { y: 0.5 }` — set the scroll position to 50% on the y axis
-   *  - `mirror.progress = { x: 0.2, y: 0.5 }` — set the scroll position on both axes
-   */
-  set progress(value: Partial<Progress> | number) {
-    /** if the value is a number, set both axes to that value */
-    if (typeof value === "number") {
-      value = { x: value, y: value };
-    }
-    const progress = { ...this.progress, ...value };
-
-    if (!this.validateProgress(progress)) {
-      return;
-    }
-
-    this.mirrorScrollPositions(progress);
-  }
-
-  /** Validate the progress, log errors for invalid values */
-  validateProgress(progress: Partial<Progress>) {
-    let valid = true;
-    for (const [key, value] of Object.entries(progress)) {
-      if (typeof value !== "number" || value < 0 || value > 1) {
-        console.error(`progress.${key} must be a number between 0-1`);
-        valid = false;
-      }
-    }
-    return valid;
-  }
-}
+import type { Options, Progress } from "./support/defs.js";
+import {
+  getScrollProgress,
+  hasCSSOverflow,
+  hasOverflow,
+  nextTick,
+} from "./support/helpers.js";
+import ScrollMirror from './ScrollMirror.js';
+
+export type { Options, Progress };
+export { getScrollProgress, hasOverflow, hasCSSOverflow, nextTick };
+export default ScrollMirror;
\ No newline at end of file
diff --git a/src/support/defs.ts b/src/support/defs.ts
new file mode 100644
index 0000000..62d3a02
--- /dev/null
+++ b/src/support/defs.ts
@@ -0,0 +1,17 @@
+import type { getLogger } from "./functions.js";
+
+export type Progress = {
+  x: number;
+  y: number;
+};
+
+export type Options = {
+  /** Mirror the vertical scroll position */
+  vertical: boolean;
+  /** Mirror the horizontal scroll position */
+  horizontal: boolean;
+  /** Enable debug messages */
+  debug: boolean;
+};
+
+export type Logger = ReturnType<typeof getLogger>;
\ No newline at end of file
diff --git a/src/support/functions.ts b/src/support/functions.ts
new file mode 100644
index 0000000..4f3b2c2
--- /dev/null
+++ b/src/support/functions.ts
@@ -0,0 +1,73 @@
+import type { Logger, Progress } from "./defs.js";
+import { hasCSSOverflow, hasOverflow } from "./helpers.js";
+
+/**
+ * Get the event target for receiving scroll events
+ * - return the window if the element is either the html or body element
+ * - otherwise, return the element
+ */
+export function getScrollEventTarget(element: HTMLElement): Window | HTMLElement {
+  return element.matches("body *") ? element : window;
+}
+
+/**
+ * Get a minimal logger with a prefix
+ */
+export function getLogger(prefix: string) {
+  return {
+    log: (...args: any[]) => console.log(prefix, ...args),
+    warn: (...args: any[]) => console.warn(prefix, ...args),
+    error: (...args: any[]) => console.error(prefix, ...args),
+  };
+}
+
+/**
+ * Make sure the provided elements are valid
+ */
+export function validateElements(
+  elements: HTMLElement[],
+  logger?: Logger
+): void {
+  if (elements.length < 1) {
+    logger?.warn("No elements provided.");
+    return;
+  }
+
+  if (elements.length < 2) {
+    logger?.warn("Only one element provided.", elements);
+  }
+
+  if (elements.some((el) => !el)) {
+    logger?.error("Some elements are not defined.", elements);
+  }
+
+  for (const element of elements) {
+    if (element instanceof HTMLElement && !hasOverflow(element)) {
+      logger?.warn("Element doesn't have overflow:", element);
+    }
+    if (
+      element instanceof HTMLElement &&
+      element.matches("body *") &&
+      !hasCSSOverflow(element)
+    ) {
+      logger?.warn('No "overflow: auto;" or "overflow: scroll;" set on element:', element); // prettier-ignore
+    }
+  }
+}
+
+/**
+ * Validate the progress, log errors for invalid values
+ */
+export function validateProgress(
+  progress: Partial<Progress>,
+  logger?: Logger
+) {
+  let valid = true;
+  for (const [key, value] of Object.entries(progress)) {
+    if (typeof value !== "number" || value < 0 || value > 1) {
+      logger?.error(`progress.${key} must be a number between 0-1`);
+      valid = false;
+    }
+  }
+  return valid;
+}
\ No newline at end of file
diff --git a/src/support/utils.ts b/src/support/helpers.ts
similarity index 51%
rename from src/support/utils.ts
rename to src/support/helpers.ts
index 067e47f..2de3a2f 100644
--- a/src/support/utils.ts
+++ b/src/support/helpers.ts
@@ -1,3 +1,5 @@
+import type { Progress } from "./defs.js";
+
 /** Return a Promise that resolves after the next event loop. */
 export const nextTick = (): Promise<void> => {
   return new Promise((resolve) => {
@@ -20,3 +22,30 @@ export const hasCSSOverflow = (element: HTMLElement) => {
   const overflow = window.getComputedStyle(element)["overflow"];
   return overflow.includes("auto") || overflow.includes("scroll");
 };
+
+/** Get the scroll progress of an element, between 0-1 */
+export const getScrollProgress = (el: HTMLElement | undefined): Progress => {
+  if (el == null) {
+    return {
+      x: 0,
+      y: 0,
+    };
+  }
+
+  const {
+    scrollTop,
+    scrollHeight,
+    clientHeight,
+    scrollLeft,
+    scrollWidth,
+    clientWidth,
+  } = el;
+
+  const availableWidth = scrollWidth - clientWidth;
+  const availableHeight = scrollHeight - clientHeight;
+
+  return {
+    x: !!scrollLeft ? scrollLeft / Math.max(0.00001, availableWidth) : 0,
+    y: !!scrollTop ? scrollTop / Math.max(0.00001, availableHeight) : 0,
+  };
+};
diff --git a/tests/unit/tests/exports.test.ts b/tests/unit/tests/exports.test.ts
new file mode 100644
index 0000000..ec7efec
--- /dev/null
+++ b/tests/unit/tests/exports.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+
+import * as ScrollMirrorModule from "../../../src/index.js";
+import ScrollMirror from "../../../src/index.js";
+import type { Options } from "../../../src/index.js";
+import * as ScrollMirrorTS from "../../../src/ScrollMirror.js";
+
+describe("Exports", () => {
+  it("should have the correct exports for the es6 module", () => {
+    expect(Object.keys(ScrollMirrorModule)).toEqual([
+      "getScrollProgress",
+      "hasOverflow",
+      "hasCSSOverflow",
+      "nextTick",
+      "default",
+    ]);
+
+    const instance = new ScrollMirrorModule.default([
+      document.createElement("div"),
+    ]);
+    expect(instance).toBeInstanceOf(ScrollMirror)
+  });
+
+  it("should only have a default export for the UMD bundle", () => {
+    expect(Object.keys(ScrollMirrorTS)).toEqual(["default"]);
+  });
+});
diff --git a/tests/unit/tests/logger.test.ts b/tests/unit/tests/logger.test.ts
new file mode 100644
index 0000000..796fe71
--- /dev/null
+++ b/tests/unit/tests/logger.test.ts
@@ -0,0 +1,45 @@
+import { vi, describe, expect, it, beforeEach, afterEach } from "vitest";
+
+import ScrollMirror from "../../../src/index.js";
+
+describe("Logger", () => {
+  let warnSpy: ReturnType<typeof vi.spyOn>;
+  let errorSpy: ReturnType<typeof vi.spyOn>;
+
+  beforeEach(() => {
+    warnSpy = vi.spyOn(console, "warn");
+    errorSpy = vi.spyOn(console, "error");
+  });
+
+  afterEach(() => {
+    warnSpy.mockRestore();
+    errorSpy.mockRestore();
+  });
+
+  it("should log if debug is true", () => {
+    const mirror = new ScrollMirror(document.querySelectorAll(":root"), {
+      debug: true,
+    });
+    expect(warnSpy).toBeCalledWith(
+      expect.anything(),
+      "Only one element provided.",
+      expect.anything()
+    );
+
+    mirror.progress = { y: 2 };
+    expect(errorSpy).toBeCalledWith(
+      expect.anything(),
+      "progress.y must be a number between 0-1"
+    );
+  });
+
+  it("should not log if debug is false", () => {
+    const mirror = new ScrollMirror(document.querySelectorAll(":root"), {
+      debug: false,
+    });
+    expect(warnSpy).not.toBeCalled();
+
+    mirror.progress = { y: 2 };
+    expect(errorSpy).not.toBeCalled();
+  });
+});
diff --git a/tests/unit/vitest.config.ts b/tests/unit/vitest.config.ts
new file mode 100644
index 0000000..5b61fb8
--- /dev/null
+++ b/tests/unit/vitest.config.ts
@@ -0,0 +1,17 @@
+/**
+ * Vitest config file
+ * @see https://vitest.dev/config/
+ */
+
+import path from 'path';
+import { defineConfig } from 'vitest/config';
+
+const __dirname = path.dirname(__filename);
+
+export default defineConfig({
+	test: {
+		environment: 'jsdom',
+		include: ['tests/unit/tests/*.test.ts'],
+		setupFiles: [path.resolve(__dirname, './vitest.setup.ts')]
+	}
+});
diff --git a/tests/unit/vitest.setup.ts b/tests/unit/vitest.setup.ts
new file mode 100644
index 0000000..2b416ba
--- /dev/null
+++ b/tests/unit/vitest.setup.ts
@@ -0,0 +1,6 @@
+import { vi } from 'vitest';
+
+// Stub browser functions for vitest
+// console.log = vi.fn();
+// console.warn = vi.fn();
+// console.error = vi.fn();