robert 2 天之前
父節點
當前提交
c83cebb99d

+ 36 - 32
README.md

@@ -16,28 +16,32 @@
 ## 📦 安装与引入
 
 ```html
-<!-- 1. 引入动画引擎 (导出全局变量 animal) -->
-<script src="animal.js"></script>
+<script src="xjs.js"></script>
+```
+
+引入后会得到这些全局变量:
+
+- `xjs`(**推荐统一使用**,也是一个 selector 函数)
+- `animal`(兼容别名,等同于 `xjs`)
+- `Layer`
+
+如果你确实想全局占用 `$`(可能与 jQuery 冲突),在引入前设置:
 
-<!-- 2. 引入弹窗组件 (导出全局变量 Layer) -->
-<script src="layer.js"></script>
+```html
+<script>window.XJS_GLOBAL_DOLLAR = true;</script>
+<script src="xjs.js"></script>
 ```
 
 ---
 
 ## 🚀 快速上手
 
-为了获得最佳体验,建议将 `animal` 赋值给 `$` (或其他你喜欢的简写)。
-
-```javascript
-// 建立简写别名
-const $ = animal;
-```
+示例中统一直接使用 `xjs(...)`,不再额外定义 `$` 等别名(避免与其他库冲突,也更清晰)。
 
 ### 1. 基础动画 (`.animate`)
 
 ```javascript
-$('.box').animate({
+xjs('.box').animate({
   x: 200,             // 自动处理 transform
   scale: 1.5,
   opacity: 0.5,
@@ -55,7 +59,7 @@ $('.box').animate({
 
 ```javascript
 // 点击按钮触发弹窗
-$('.btn-delete').layer({
+xjs('.btn-delete').layer({
   title: '确定删除?',
   text: '此操作不可恢复',
   icon: 'warning',
@@ -66,7 +70,7 @@ $('.btn-delete').layer({
 ### 3. SVG 描边 (`.draw`)
 
 ```javascript
-$('path').draw({ 
+xjs('path').draw({ 
   duration: 1500,
   easing: 'ease-in-out'
 });
@@ -82,7 +86,7 @@ $('path').draw({
 无需配置复杂的贝塞尔曲线,使用物理参数即可。
 
 ```javascript
-$('.card').animate({
+xjs('.card').animate({
   y: [100, 0],
   easing: { stiffness: 200, damping: 10 }
 });
@@ -92,7 +96,7 @@ $('.card').animate({
 元素进入屏幕可视区域时自动播放。
 
 ```javascript
-$('.fade-up').inViewAnimate({
+xjs('.fade-up').inViewAnimate({
   y: [50, 0],
   opacity: [0, 1],
   duration: 600
@@ -103,20 +107,20 @@ $('.fade-up').inViewAnimate({
 将动画进度绑定到页面滚动条。
 
 ```javascript
-const controls = $('.progress').animate({ 
+const controls = xjs('.progress').animate({ 
   width: ['0%', '100%'], 
   autoplay: false 
 });
 
-// 使用静态方法 $.scroll 绑定
-$.scroll(controls);
+// 使用静态方法 xjs.scroll 绑定
+xjs.scroll(controls);
 ```
 
 #### 时间轴 (Timeline)
 编排串行复杂的动画序列。
 
 ```javascript
-const tl = $.timeline();
+const tl = xjs.timeline();
 
 tl.add('.box-1', { x: 100 })
   .add('.box-2', { x: 100 }, '-=200') // 提前 200ms
@@ -127,24 +131,24 @@ tl.add('.box-1', { x: 100 })
 
 ### Layer: 弹出层
 
-虽然推荐使用 `$('.el').layer(...)` 绑定事件,但你也完全可以通过全局对象手动调用。
+虽然推荐使用 `xjs('.el').layer(...)` 绑定事件,但你也完全可以通过全局对象手动调用。
 
-#### 基础弹窗 (直接调用 $.layer)
+#### 基础弹窗 (直接调用 xjs.layer)
 ```javascript
-$.layer({
+xjs.layer({
   title: 'Hello World',
-  text: 'This is a message from $.layer()'
+  text: 'This is a message from xjs.layer()'
 });
 ```
 
 #### Confirm 确认流程
 ```javascript
-$.layer({
+xjs.layer({
   title: '提交表单?',
   showCancelButton: true
 }).then((res) => {
   if (res.isConfirmed) {
-    $.layer({ title: '提交成功!', icon: 'success' });
+    xjs.layer({ title: '提交成功!', icon: 'success' });
   }
 });
 ```
@@ -152,13 +156,13 @@ $.layer({
 #### 弹窗内容支持 DOM/HTML
 ```javascript
 // 传入 DOM 元素
-$.layer({
+xjs.layer({
   title: 'Custom Content',
   content: document.querySelector('#my-template')
 });
 
 // 或者传入 HTML 字符串
-$.layer({
+xjs.layer({
   html: '<strong>Bold Text</strong><br>New line'
 });
 ```
@@ -181,12 +185,12 @@ $.layer({
 
 | 调用方式 | 描述 |
 | :--- | :--- |
-| **`$(selector)`** | **核心选择器**,返回 Selection 对象 |
+| **`xjs(selector)`** | **核心选择器**,返回 Selection 对象 |
 | `Selection.animate(params)` | 执行动画 |
 | `Selection.draw(params)` | SVG 描边动画 |
 | `Selection.layer(options)` | 绑定点击弹窗 |
 | `Selection.inViewAnimate(...)` | 进入视口触发动画 |
-| **`$.spring(config)`** | 创建物理缓动函数 |
-| **`$.timeline()`** | 创建时间轴 |
-| **`$.scroll(controls)`** | 绑定滚动驱动 |
-| **`$.layer(options)`** | **直接弹出窗口 (别名 $.fire)** |
+| **`xjs.spring(config)`** | 创建物理缓动函数 |
+| **`xjs.timeline()`** | 创建时间轴 |
+| **`xjs.scroll(controls)`** | 绑定滚动驱动 |
+| **`xjs.layer(options)`** | **直接弹出窗口 (别名 xjs.fire)** |

+ 13 - 16
doc/animal.html

@@ -50,14 +50,14 @@
         <h2>1. Basic Transform & Opacity (WAAPI)</h2>
         <div class="box box-1"></div>
         <button class="btn" onclick="runBasic()">Animate</button>
-        <pre>$('.box-1').animate({ x: 200, rotate: 180, opacity: 0.5, duration: 1000 })</pre>
+        <pre>xjs('.box-1').animate({ x: 200, rotate: 180, opacity: 0.5, duration: 1000 })</pre>
     </section>
 
     <section>
         <h2>2. Spring Physics</h2>
         <div class="box box-2"></div>
         <button class="btn" onclick="runSpring()">Spring</button>
-        <pre>$('.box-2').animate({ x: 200, easing: { stiffness: 200, damping: 10 } })</pre>
+        <pre>xjs('.box-2').animate({ x: 200, easing: { stiffness: 200, damping: 10 } })</pre>
     </section>
 
     <section>
@@ -95,14 +95,11 @@
         <div style="height: 50vh;"></div>
     </section>
 
-    <script src="../animal.js"></script>
+    <script src="../xjs.js"></script>
     <script>
-        // Alias
-        const $ = animal;
-
         // 1. Basic
         function runBasic() {
-            $('.box-1').animate({ 
+            xjs('.box-1').animate({ 
                 x: 200, 
                 rotate: 180, 
                 opacity: 0.5, 
@@ -111,13 +108,13 @@
             }).then(() => {
                 console.log('Basic finished');
                 // Chain back
-                $('.box-1').animate({ x: 0, rotate: 0, opacity: 1, duration: 500 });
+                xjs('.box-1').animate({ x: 0, rotate: 0, opacity: 1, duration: 500 });
             });
         }
 
         // 2. Spring
         function runSpring() {
-            $('.box-2').animate({ 
+            xjs('.box-2').animate({ 
                 x: 200, 
                 easing: { stiffness: 150, damping: 8 } 
             });
@@ -125,7 +122,7 @@
 
         // 3. Timeline
         function runTimeline() {
-            const tl = $.timeline();
+            const tl = xjs.timeline();
             tl.add('.box-3a', { x: 100, duration: 500 })
               .add('.box-3b', { x: 100, rotate: 90, duration: 800 }, "-=200") // Overlap
               .add('.box-3a', { x: 0, duration: 500 })
@@ -135,7 +132,7 @@
 
         // 4. SVG
         function runSvg() {
-            $('.path-1').draw({ duration: 1500, easing: 'ease-in-out' });
+            xjs('.path-1').draw({ duration: 1500, easing: 'ease-in-out' });
         }
 
         // 5. In View (Trigger)
@@ -145,7 +142,7 @@
              // Alternate left/right entrance
              const xStart = i % 2 === 0 ? -100 : 100;
              // inViewAnimate() returns cleanup; call it if you use once:false or if you need to teardown manually.
-             $(card).inViewAnimate({
+             xjs(card).inViewAnimate({
                 x: [xStart, 0],
                 opacity: [0, 1],
                 scale: [0.8, 1],
@@ -157,15 +154,15 @@
 
         // 6. Scroll Linked
         // 1. Progress Bar (Page Scroll)
-        const progressAnim = $('.progress-bar').animate({
+        const progressAnim = xjs('.progress-bar').animate({
             width: ['0%', '100%'],
             duration: 1000, // Duration doesn't matter much for scroll linked, just maps 0-1
             autoplay: false
         });
-        $.scroll(progressAnim); // Default: window scroll
+        xjs.scroll(progressAnim); // Default: window scroll
 
         // 2. Box Rotation (Element Scroll Visibility)
-        const boxAnim = $('.scroll-box').animate({
+        const boxAnim = xjs('.scroll-box').animate({
             rotate: 360,
             x: 200,
             backgroundColor: '#e74c3c',
@@ -177,7 +174,7 @@
         // animal.js current `scroll` implementation supports `target` for element visibility progress!
         
         // Let's link it to the page scroll for clarity
-        $.scroll(boxAnim);
+        xjs.scroll(boxAnim);
 
     </script>
 </body>

+ 9 - 12
doc/animal_basic.html

@@ -213,11 +213,9 @@
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="str">'17rem'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'#css-selector-id'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> rotate<span class="punc">:</span> <span class="str">'1turn'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.row:nth-child(3) .square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> scale<span class="punc">:</span> <span class="punc">[</span><span class="num">1</span><span class="punc">,</span> <span class="num">.5</span><span class="punc">]</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="str">'17rem'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#css-selector-id'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> rotate<span class="punc">:</span> <span class="str">'1turn'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.row:nth-child(3) .square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> scale<span class="punc">:</span> <span class="punc">[</span><span class="num">1</span><span class="punc">,</span> <span class="num">.5</span><span class="punc">]</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
             </div>
 
             <!-- HTML Code View -->
@@ -256,7 +254,7 @@
 
     </div>
 
-    <script src="../animal.js"></script>
+    <script src="../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -277,29 +275,28 @@
                 el.style.transform = 'none';
             });
             
-            const $ = animal;
-            
+                        
             // 1. Move all squares
-            $('.square').animate({ 
+            xjs('.square').animate({ 
                 x: '17rem', 
                 duration: 1000,
                 easing: 'ease-out'
             });
 
             // 2. Rotate the ID one
-            $('#css-selector-id').animate({ 
+            xjs('#css-selector-id').animate({ 
                 rotate: '1turn', 
                 duration: 1000,
                 easing: 'ease-out'
             });
 
             // 3. Scale the 3rd row square
-            $('.medium.row:nth-child(3) .square').animate({
+            xjs('.medium.row:nth-child(3) .square').animate({
                 scale: [1, .5],
                 duration: 500,
                 easing: 'ease-in-out'
             }).then(() => {
-                $('.medium.row:nth-child(3) .square').animate({
+                xjs('.medium.row:nth-child(3) .square').animate({
                     scale: [.5, 1],
                     duration: 500,
                     easing: 'ease-in-out'

+ 4 - 7
doc/animatable_properties/test_css_props.html

@@ -35,9 +35,7 @@
             </div>
 
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.prop-box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.prop-box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   width<span class="punc">:</span> <span class="str">'100px'</span><span class="punc">,</span>
   backgroundColor<span class="punc">:</span> <span class="str">'#FFF'</span><span class="punc">,</span>
   borderRadius<span class="punc">:</span> <span class="str">'50%'</span>
@@ -59,7 +57,7 @@
 
     </div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -78,9 +76,8 @@
             el.style.backgroundColor = ''; // Reset to CSS var
             el.style.borderRadius = '4px';
             
-            const $ = animal;
-            
-            $('.prop-box').animate({ 
+                        
+            xjs('.prop-box').animate({ 
                 width: '100px',
                 backgroundColor: '#FFF', 
                 borderRadius: '50%',

+ 4 - 6
doc/animatable_properties/test_css_vars.html

@@ -29,8 +29,7 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   <span class="str">'--box-width'</span><span class="punc">:</span> <span class="str">'200px'</span><span class="punc">,</span>
   duration<span class="punc">:</span> <span class="num">1000</span><span class="punc">,</span>
   easing<span class="punc">:</span> <span class="str">'ease-in-out'</span>
@@ -42,14 +41,13 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
-            const $ = animal;
-            const box = document.querySelector('.box');
+                        const box = document.querySelector('.box');
             box.style.setProperty('--box-width', '50px');
 
-            $('.box').animate({
+            xjs('.box').animate({
                 '--box-width': '200px',
                 duration: 1000,
                 easing: 'ease-in-out'

+ 3 - 3
doc/animatable_properties/test_js_props.html

@@ -21,9 +21,9 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ <span class="punc">=</span> <span class="fun">animal</span><span class="punc">;</span>
+<span class="kwd">const</span> $ <span class="punc">=</span> <span class="fun">xjs</span><span class="punc">;</span>
 <span class="kwd">const</span> <span class="val">obj</span> <span class="punc">=</span> <span class="punc">{</span> score<span class="punc">:</span> <span class="num">0</span> <span class="punc">}</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="val">obj</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="val">obj</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   score<span class="punc">:</span> <span class="num">1000</span><span class="punc">,</span>
   update<span class="punc">:</span> <span class="punc">()</span> <span class="punc">=></span> updateUI(Math.round(obj.score))
 <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
@@ -34,7 +34,7 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             const obj = { score: 0 };

+ 3 - 6
doc/animatable_properties/test_transforms.html

@@ -26,9 +26,7 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   translateX<span class="punc">:</span> <span class="num">100</span><span class="punc">,</span>
   rotate<span class="punc">:</span> <span class="str">'1turn'</span><span class="punc">,</span>
   scale<span class="punc">:</span> <span class="num">0.5</span>
@@ -40,12 +38,11 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             document.querySelector('.box').style.transform = 'none';
-            const $ = animal;
-            $('.box').animate({ translateX: 100, rotate: '1turn', scale: 0.5, duration: 1000 });
+                        xjs('.box').animate({ translateX: 100, rotate: '1turn', scale: 0.5, duration: 1000 });
         }
         setTimeout(runDemo, 500);
     </script>

+ 8 - 11
doc/examples/controls.html

@@ -68,9 +68,7 @@
                 </div>
             </div>
 
-            <pre id="js-code" class="code-view active">const $ = animal;
-
-const ctl = $('.ex-box').animate({
+            <pre id="js-code" class="code-view active">const ctl = xjs('.ex-box').animate({
   x: 220,
   rotate: 180,
   opacity: 0.6,
@@ -85,12 +83,12 @@ ctl.seek(0.25).play();
 ctl.then(() =&gt; console.log('done'));
 
 // scroll-linked (works with WAAPI + JS fallback)
-const progress = $('.ex-bar').animate({
+const progress = xjs('.ex-bar').animate({
   width: ['0%', '100%'],
   autoplay: false,
   duration: 1000
 });
-$.scroll(progress);</pre>
+xjs.scroll(progress);</pre>
 
             <pre id="html-code" class="html-view">&lt;div class="ex-box"&gt;&lt;/div&gt;
 &lt;div class="bar-wrap"&gt;
@@ -115,7 +113,7 @@ $.scroll(progress);</pre>
         </div>
     </div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -131,8 +129,7 @@ $.scroll(progress);</pre>
 
         let cleanup = null;
         function runDemo() {
-            const $ = animal;
-            // reset
+                        // reset
             const box = document.querySelector('.ex-box');
             const bar = document.querySelector('.ex-bar');
             box.style.transform = 'none';
@@ -143,7 +140,7 @@ $.scroll(progress);</pre>
             if (cleanup) cleanup();
             cleanup = null;
 
-            const ctl = $('.ex-box').animate({
+            const ctl = xjs('.ex-box').animate({
                 x: 220,
                 rotate: 180,
                 opacity: 0.6,
@@ -152,12 +149,12 @@ $.scroll(progress);</pre>
             });
             ctl.seek(0.25).play();
 
-            const progress = $('.ex-bar').animate({
+            const progress = xjs('.ex-bar').animate({
                 width: ['0%', '100%'],
                 autoplay: false,
                 duration: 1000
             });
-            cleanup = $.scroll(progress);
+            cleanup = xjs.scroll(progress);
         }
 
         setTimeout(runDemo, 300);

+ 12 - 17
doc/examples/layer.html

@@ -83,10 +83,8 @@
         </div>
       </div>
 
-      <pre id="js-code" class="code-view active">const $ = animal;
-
-// 1) Bind click popup (recommended for buttons / links)
-$('.btn-delete').layer({
+      <pre id="js-code" class="code-view active">// 1) Bind click popup (recommended for buttons / links)
+xjs('.btn-delete').layer({
   title: 'Delete item?',
   text: 'This action cannot be undone.',
   icon: 'warning',
@@ -94,11 +92,11 @@ $('.btn-delete').layer({
 });
 
 // 2) Bind from data-* (no JS options needed)
-$('.btn-data').layer(); // reads data-layer-* attributes
+xjs('.btn-data').layer(); // reads data-layer-* attributes
 
 // 3) Fire directly (manual usage)
 document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
-  $.layer({ title: 'Hello from $.layer()', icon: 'success' });
+  xjs.layer({ title: 'Hello from xjs.layer()', icon: 'success' });
 });</pre>
 
       <pre id="html-code" class="html-view">&lt;button class="btn danger btn-delete"&gt;Delete (bind .layer)&lt;/button&gt;
@@ -110,7 +108,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
   data-layer-icon="success"
 &gt;Open (data-layer-*)&lt;/button&gt;
 
-&lt;button class="btn btn-direct"&gt;Open ($.layer)&lt;/button&gt;</pre>
+&lt;button class="btn btn-direct"&gt;Open (xjs.layer)&lt;/button&gt;</pre>
 
       <div class="demo-visual">
         <div class="row">
@@ -123,11 +121,11 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
             data-layer-icon="success"
           >Open (data-layer-*)</button>
 
-          <button class="btn btn-direct">Open ($.layer)</button>
+          <button class="btn btn-direct">Open (xjs.layer)</button>
         </div>
         <div class="hint">
           Make sure you included both <code class="inline">animal.js</code> and <code class="inline">layer.js</code>.
-          For click-binding, <span class="pill">$('.btn').layer(options)</span> will de-dupe old handlers if called again.
+          For click-binding, <span class="pill">xjs('.btn').layer(options)</span> will de-dupe old handlers if called again.
         </div>
       </div>
 
@@ -137,8 +135,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
     </div>
   </div>
 
-  <script src="../../animal.js"></script>
-  <script src="../../layer.js"></script>
+  <script src="../../xjs.js"></script>
   <script>
     function switchTab(tab) {
       document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -153,10 +150,8 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
     }
 
     function runDemo() {
-      const $ = animal;
-
-      // bind 1) explicit options
-      $('.btn-delete').layer({
+            // bind 1) explicit options
+      xjs('.btn-delete').layer({
         title: 'Delete item?',
         text: 'This action cannot be undone.',
         icon: 'warning',
@@ -164,13 +159,13 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
       });
 
       // bind 2) data-* driven
-      $('.btn-data').layer();
+      xjs('.btn-data').layer();
 
       // 3) direct fire
       const direct = document.querySelector('.btn-direct');
       if (direct) {
         direct.onclick = () => {
-          $.layer({ title: 'Hello from $.layer()', text: 'Direct call (alias of $.fire)', icon: 'success' });
+          xjs.layer({ title: 'Hello from xjs.layer()', text: 'Direct call (alias of xjs.fire)', icon: 'success' });
         };
       }
     }

+ 1 - 1
doc/examples/list.html

@@ -59,7 +59,7 @@
 
             <div class="card" data-content="examples/layer.html" onclick="selectItem('layer.html', this)">
                 <div class="card-preview">
-                    <div class="preview-pill">$('.btn').layer({ title: 'Hi' })</div>
+                    <div class="preview-pill">xjs('.btn').layer({ title: 'Hi' })</div>
                 </div>
                 <div class="card-footer">Layer (popup / bind click)</div>
             </div>

+ 3 - 4
doc/search.html

@@ -69,10 +69,9 @@
         </div>
     </div>
     
-    <script src="../animal.js"></script>
+    <script src="../xjs.js"></script>
     <script>
-        const $ = animal;
-        // --- Rotation Logic ---
+                // --- Rotation Logic ---
         let jsRotations = 0;
         let reactRotations = 0;
 
@@ -90,7 +89,7 @@
             let current = (iconId === 'js-icon') ? ++jsRotations : ++reactRotations;
             
             // Use animal.js to rotate
-            $(icon).animate({
+            xjs(icon).animate({
                 rotate: current * 360, 
                 duration: 600,
                 easing: 'ease-out'

+ 2 - 2
doc/targets/overview.html

@@ -43,8 +43,8 @@
 
         <div class="box-container" style="margin-top: 30px;">
             <div class="code-view active" style="background: #161616; padding: 40px;">
-                <span class="kwd">const</span> $ <span class="punc">=</span> <span class="fun">animal</span>;<br><br>
-                <span class="fun">$</span>(<span class="str">'.square'</span>)<span class="annotation">Targets</span>.<span class="fun">animate</span>(<br>
+                <span class="kwd">const</span> $ <span class="punc">=</span> <span class="fun">xjs</span>;<br><br>
+                <span class="fun">xjs</span>(<span class="str">'.square'</span>)<span class="annotation">Targets</span>.<span class="fun">animate</span>(<br>
                 &nbsp;&nbsp;{<br>
                 &nbsp;&nbsp;&nbsp;&nbsp;x: <span class="num">100</span>,<br>
                 &nbsp;&nbsp;&nbsp;&nbsp;scale: <span class="num">2</span>,<br>

+ 4 - 7
doc/targets/test_array_targets.html

@@ -39,12 +39,10 @@
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
 <span class="kwd">const</span> <span class="val">el1</span> <span class="punc">=</span> document.querySelector<span class="punc">(</span><span class="str">'.el-1'</span><span class="punc">);</span>
 <span class="kwd">const</span> <span class="val">el2</span> <span class="punc">=</span> document.querySelector<span class="punc">(</span><span class="str">'.el-2'</span><span class="punc">);</span>
 
-<span class="fun">$</span><span class="punc">(</span><span class="punc">[</span><span class="val">el1</span><span class="punc">,</span> <span class="val">el2</span><span class="punc">]</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="num">200</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="punc">[</span><span class="val">el1</span><span class="punc">,</span> <span class="val">el2</span><span class="punc">]</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="num">200</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
             </div>
 
             <!-- HTML Code View -->
@@ -66,7 +64,7 @@
 
     </div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -87,11 +85,10 @@
                 el.style.transform = 'none';
             });
             
-            const $ = animal;
-            const el1 = document.querySelector('.el-1');
+                        const el1 = document.querySelector('.el-1');
             const el2 = document.querySelector('.el-2');
             
-            $([el1, el2]).animate({ 
+            xjs([el1, el2]).animate({ 
                 x: 200, 
                 duration: 1000,
                 easing: 'ease-out'

+ 8 - 12
doc/targets/test_css_selector.html

@@ -91,11 +91,9 @@
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="str">'17rem'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'#css-selector-id'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> rotate<span class="punc">:</span> <span class="str">'1turn'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.row:nth-child(3) .square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="str">'17rem'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#css-selector-id'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> rotate<span class="punc">:</span> <span class="str">'1turn'</span> <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.row:nth-child(3) .square'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   scale<span class="punc">:</span> <span class="punc">[</span><span class="num">1</span><span class="punc">,</span> <span class="num">.5</span><span class="punc">]</span>
 <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
             </div>
@@ -156,7 +154,7 @@
 
     <div id="toast" class="toast" role="status" aria-live="polite"></div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -215,26 +213,24 @@
                 el.style.transform = 'none';
             });
             
-            const $ = animal;
-
-            $('.square').animate({ 
+                        xjs('.square').animate({ 
                 x: '17rem', 
                 duration: 1000,
                 easing: 'ease-out'
             });
 
-            $('#css-selector-id').animate({ 
+            xjs('#css-selector-id').animate({ 
                 rotate: '1turn', 
                 duration: 1000,
                 easing: 'ease-out'
             });
 
-            $('.medium.row:nth-child(3) .square').animate({
+            xjs('.medium.row:nth-child(3) .square').animate({
                 scale: [1, .5],
                 duration: 500,
                 easing: 'ease-in-out'
             }).then(() => {
-                $('.medium.row:nth-child(3) .square').animate({
+                xjs('.medium.row:nth-child(3) .square').animate({
                     scale: [.5, 1],
                     duration: 500,
                     easing: 'ease-in-out'

+ 6 - 9
doc/targets/test_dom_elements.html

@@ -39,16 +39,14 @@
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
 <span class="kwd">const</span> <span class="val">element</span> <span class="punc">=</span> document.querySelector<span class="punc">(</span><span class="str">'.dom-node'</span><span class="punc">);</span>
 <span class="kwd">const</span> <span class="val">elements</span> <span class="punc">=</span> document.querySelectorAll<span class="punc">(</span><span class="str">'.dom-node-list'</span><span class="punc">);</span>
 
-<span class="fun">$</span><span class="punc">(</span><span class="val">element</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="val">element</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   x<span class="punc">:</span> <span class="num">200</span>
 <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
 
-<span class="fun">$</span><span class="punc">(</span><span class="val">elements</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="val">elements</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   x<span class="punc">:</span> <span class="num">200</span>
 <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
             </div>
@@ -74,7 +72,7 @@
 
     </div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -94,17 +92,16 @@
                 el.style.transform = 'none';
             });
             
-            const $ = animal;
-            const element = document.querySelector('.dom-node');
+                        const element = document.querySelector('.dom-node');
             const elements = document.querySelectorAll('.dom-node-list');
             
-            $(element).animate({ 
+            xjs(element).animate({ 
                 x: 200, 
                 duration: 1000,
                 easing: 'ease-out'
             });
 
-            $(elements).animate({ 
+            xjs(elements).animate({ 
                 x: 200, 
                 duration: 1000,
                 delay: 200,

+ 4 - 7
doc/targets/test_js_objects.html

@@ -33,11 +33,9 @@
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
 <span class="kwd">const</span> <span class="val">obj</span> <span class="punc">=</span> <span class="punc">{</span> prop<span class="punc">:</span> <span class="num">0</span> <span class="punc">}</span><span class="punc">;</span>
 
-<span class="fun">$</span><span class="punc">(</span><span class="val">obj</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="val">obj</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   prop<span class="punc">:</span> <span class="num">100</span><span class="punc">,</span>
   update<span class="punc">:</span> <span class="punc">()</span> <span class="punc">=></span> console.log(obj.prop)
 <span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
@@ -62,7 +60,7 @@
 
     </div>
 
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function switchTab(tab) {
             document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -80,9 +78,8 @@
         function runDemo() {
             const logEl = document.getElementById('log-val');
             const obj = { prop: 0 };
-            const $ = animal;
-            
-            $(obj).animate({ 
+                        
+            xjs(obj).animate({ 
                 prop: 100, 
                 duration: 2000,
                 easing: 'linear',

+ 5 - 9
doc/test_on_update.html

@@ -50,8 +50,7 @@
             <div style="font-weight: bold; color: #2196F3; margin-bottom: 5px; font-size: 10px; letter-spacing: 1px;">INFO</div>
             <div style="color: #90caf9; margin-bottom: 10px;">Callbacks are configured per animation via the params object.</div>
             <div style="background: rgba(0,0,0,0.3); padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; color: #a9b7c6; white-space: pre-wrap;">
-const $ = animal;
-$('.box').animate({
+xjs('.box').animate({
   x: 200,
   update: ({ progress }) => console.log(progress)
 });
@@ -69,13 +68,11 @@ $('.box').animate({
 
             <!-- JS Code View -->
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-
 <span class="kwd">const</span> $value <span class="punc">=</span> document.querySelector<span class="punc">(</span><span class="str">'.value'</span><span class="punc">);</span>
 
 <span class="kwd">let</span> updates <span class="punc">=</span> <span class="num">0</span><span class="punc">;</span>
 
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.circle'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.circle'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
   x<span class="punc">:</span> <span class="str">'16rem'</span><span class="punc">,</span>
   loop<span class="punc">:</span> Infinity<span class="punc">,</span>
   direction<span class="punc">:</span> <span class="str">'alternate'</span><span class="punc">,</span>
@@ -105,7 +102,7 @@ $('.box').animate({
 
     </div>
 
-    <script src="../animal.js"></script>
+    <script src="../xjs.js"></script>
     <script>
         const utils = {
             $: (selector) => document.querySelectorAll(selector)
@@ -125,13 +122,12 @@ $('.box').animate({
         }
 
         function runDemo() {
-            const $ = animal;
-            const $value = document.querySelector('.value');
+                        const $value = document.querySelector('.value');
             const $circle = document.querySelector('.circle');
             $circle.style.transform = 'none';
             $value.textContent = '0';
 
-            $('.circle').animate({
+            xjs('.circle').animate({
                 x: '16rem',
                 duration: 1000,
                 loop: Infinity, 

+ 3 - 5
doc/tween_value_types/test_colors.html

@@ -26,8 +26,7 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   backgroundColor<span class="punc">:</span> <span class="str">'#FFF'</span><span class="punc">,</span>
   duration<span class="punc">:</span> <span class="num">1000</span><span class="punc">,</span>
   direction<span class="punc">:</span> <span class="str">'alternate'</span><span class="punc">,</span>
@@ -40,14 +39,13 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             const box = document.querySelector('.box');
             box.style.backgroundColor = ''; // Reset
             
-            const $ = animal;
-            $('.box').animate({
+                        xjs('.box').animate({
                 backgroundColor: '#FFF',
                 duration: 1000,
                 direction: 'alternate',

+ 3 - 5
doc/tween_value_types/test_numerical.html

@@ -26,8 +26,7 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   x<span class="punc">:</span> <span class="num">200</span><span class="punc">,</span>
   scale<span class="punc">:</span> <span class="num">2</span><span class="punc">,</span>
   duration<span class="punc">:</span> <span class="num">1000</span><span class="punc">,</span>
@@ -40,12 +39,11 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             document.querySelector('.box').style.transform = 'none';
-            const $ = animal;
-            $('.box').animate({ x: 200, scale: 2, duration: 1000, easing: 'ease-out' });
+                        xjs('.box').animate({ x: 200, scale: 2, duration: 1000, easing: 'ease-out' });
         }
         setTimeout(runDemo, 500);
     </script>

+ 4 - 6
doc/tween_value_types/test_relative.html

@@ -29,11 +29,10 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
 <span class="kwd">const</span> box <span class="punc">=</span> document.querySelector<span class="punc">(</span><span class="str">'.box'</span><span class="punc">);</span>
 <span class="kwd">const</span> w <span class="punc">=</span> parseFloat<span class="punc">(</span>getComputedStyle<span class="punc">(</span>box<span class="punc">)</span>.width<span class="punc">)</span> <span class="punc">||</span> <span class="num">50</span><span class="punc">;</span>
 
-<span class="fun">$</span><span class="punc">(</span>box<span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span>box<span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   x<span class="punc">:</span> <span class="num">100</span><span class="punc">,</span>
   width<span class="punc">:</span> <span class="punc">[</span><span class="str">''</span> <span class="punc">+</span> w <span class="punc">+</span> <span class="str">'px'</span><span class="punc">,</span> <span class="str">''</span> <span class="punc">+</span> <span class="punc">(</span>w <span class="punc">-</span> <span class="num">10</span><span class="punc">)</span> <span class="punc">+</span> <span class="str">'px'</span><span class="punc">]</span><span class="punc">,</span>
   duration<span class="punc">:</span> <span class="num">1000</span>
@@ -45,17 +44,16 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             const box = document.querySelector('.box');
             box.style.transform = 'none';
             box.style.width = '50px';
 
-            const $ = animal;
-            const w = parseFloat(getComputedStyle(box).width) || 50;
+                        const w = parseFloat(getComputedStyle(box).width) || 50;
 
-            $(box).animate({
+            xjs(box).animate({
                 x: 100,
                 width: [w + 'px', (w - 10) + 'px'],
                 duration: 1000,

+ 3 - 5
doc/tween_value_types/test_unit.html

@@ -26,8 +26,7 @@
                 <div class="tabs"><div class="tab active">JavaScript</div></div>
             </div>
             <div id="js-code" class="code-view active">
-<span class="kwd">const</span> $ = <span class="fun">animal</span><span class="punc">;</span>
-<span class="fun">$</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.box'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">(</span><span class="punc">{</span>
   width<span class="punc">:</span> <span class="str">'100%'</span><span class="punc">,</span> <span class="com">// from 50px</span>
   duration<span class="punc">:</span> <span class="num">1000</span><span class="punc">,</span>
   easing<span class="punc">:</span> <span class="str">'ease-in-out'</span>
@@ -39,14 +38,13 @@
             <div class="action-bar"><button class="play-btn" onclick="runDemo()">REPLAY</button></div>
         </div>
     </div>
-    <script src="../../animal.js"></script>
+    <script src="../../xjs.js"></script>
     <script>
         function runDemo() {
             const box = document.querySelector('.box');
             box.style.width = '50px'; // Reset
             
-            const $ = animal;
-            $('.box').animate({
+                        xjs('.box').animate({
                 width: '100%',
                 duration: 1000,
                 easing: 'ease-in-out'

+ 1 - 1
test.html

@@ -54,7 +54,7 @@
 
     <div id="result"></div>
 
-    <script src="layer.js"></script>
+    <script src="xjs.js"></script>
     <script>
         function log(msg) {
             document.getElementById('result').textContent = msg;

+ 1728 - 14
xjs.js

@@ -1,21 +1,1735 @@
-// xjs.js - Combined build (example)
-// This file is auto-generated/merged in production.
-// For dev, it imports animal and layer from window if loaded separately.
+// xjs.js - combined single-file build (animal + layer)
+// Generated from source files in this repo.
+// Usage:
+//   <script src="xjs.js"></script>
+// Globals:
+//   - window.xjs (primary, same as animal)
+//   - window.animal (compat)
+//   - window.Layer
+// Optional:
+//   - set window.XJS_GLOBAL_DOLLAR = true before loading to also export window.$ (only if not already defined)
 
-(function(global) {
-  // If we are in a merged environment, Layer and animal might be defined in closures above.
-  // But here we simulate the final structure.
+(function(){
+  var __GLOBAL__ = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : this);
+
+
+/* --- layer.js --- */
+(function (global, factory) {
+  const LayerClass = factory();
+  // Allow usage as `Layer({...})` or `new Layer()`
+  // But Layer is a class. We can wrap it in a proxy or factory function.
+  
+  function LayerFactory(options) {
+     if (options && typeof options === 'object') {
+         return LayerClass.$(options);
+     }
+     return new LayerClass();
+  }
+  
+  // Copy static methods
+  Object.assign(LayerFactory, LayerClass);
+  // Also copy prototype for instanceof checks if needed (though tricky with factory)
+  LayerFactory.prototype = LayerClass.prototype;
+  
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
+    typeof define === 'function' && define.amd ? define(() => LayerFactory) :
+      (global.Layer = LayerFactory);
+}(__GLOBAL__, (function () {
+  'use strict';
+
+  const PREFIX = 'layer-';
+  
+  // Theme & Styles
+  const css = `
+    .${PREFIX}overlay {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.4);
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      z-index: 1000;
+      opacity: 0;
+      transition: opacity 0.3s;
+    }
+    .${PREFIX}overlay.show {
+      opacity: 1;
+    }
+    .${PREFIX}popup {
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+      width: 32em;
+      max-width: 90%;
+      padding: 1.5em;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      transform: scale(0.9);
+      transition: transform 0.3s;
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    }
+    .${PREFIX}overlay.show .${PREFIX}popup {
+      transform: scale(1);
+    }
+    .${PREFIX}title {
+      font-size: 1.8em;
+      font-weight: 600;
+      color: #333;
+      margin: 0 0 0.5em;
+      text-align: center;
+    }
+    .${PREFIX}content {
+      font-size: 1.125em;
+      color: #545454;
+      margin-bottom: 1.5em;
+      text-align: center;
+      line-height: 1.5;
+    }
+    .${PREFIX}actions {
+      display: flex;
+      justify-content: center;
+      gap: 1em;
+      width: 100%;
+    }
+    .${PREFIX}button {
+      border: none;
+      border-radius: 4px;
+      padding: 0.6em 1.2em;
+      font-size: 1em;
+      cursor: pointer;
+      transition: background-color 0.2s, box-shadow 0.2s;
+      color: #fff;
+    }
+    .${PREFIX}confirm {
+      background-color: #3085d6;
+    }
+    .${PREFIX}confirm:hover {
+      background-color: #2b77c0;
+    }
+    .${PREFIX}cancel {
+      background-color: #aaa;
+    }
+    .${PREFIX}cancel:hover {
+      background-color: #999;
+    }
+    .${PREFIX}icon {
+      width: 5em;
+      height: 5em;
+      border: 0.25em solid transparent;
+      border-radius: 50%;
+      margin: 1.5em auto 1.2em;
+      box-sizing: content-box;
+      position: relative;
+    }
+    
+    /* Success Icon */
+    .${PREFIX}icon.success {
+      border-color: #a5dc86;
+      color: #a5dc86;
+    }
+    .${PREFIX}icon.success::before, .${PREFIX}icon.success::after {
+      content: '';
+      position: absolute;
+      background: #fff;
+      border-radius: 50%;
+    }
+    .${PREFIX}success-line-tip {
+      width: 1.5em;
+      height: 0.3em;
+      background-color: #a5dc86;
+      display: block;
+      position: absolute;
+      top: 2.85em;
+      left: 0.85em;
+      transform: rotate(45deg);
+      border-radius: 0.15em;
+    }
+    .${PREFIX}success-line-long {
+      width: 2.9em;
+      height: 0.3em;
+      background-color: #a5dc86;
+      display: block;
+      position: absolute;
+      top: 2.4em;
+      right: 0.5em;
+      transform: rotate(-45deg);
+      border-radius: 0.15em;
+    }
+
+    /* Error Icon */
+    .${PREFIX}icon.error {
+      border-color: #f27474;
+      color: #f27474;
+    }
+    .${PREFIX}error-x-mark {
+      position: relative;
+      display: block;
+    }
+    .${PREFIX}error-line {
+      position: absolute;
+      height: 0.3em;
+      width: 3em;
+      background-color: #f27474;
+      display: block;
+      top: 2.3em;
+      left: 1em;
+      border-radius: 0.15em;
+    }
+    .${PREFIX}error-line.left {
+      transform: rotate(45deg);
+    }
+    .${PREFIX}error-line.right {
+      transform: rotate(-45deg);
+    }
+    
+    /* Warning Icon */
+    .${PREFIX}icon.warning {
+      border-color: #facea8;
+      color: #f8bb86;
+    }
+    .${PREFIX}warning-body {
+      position: absolute;
+      width: 0.3em;
+      height: 2.9em;
+      background-color: #f8bb86;
+      left: 50%;
+      top: 0.6em;
+      margin-left: -0.15em;
+      border-radius: 0.15em;
+    }
+    .${PREFIX}warning-dot {
+      position: absolute;
+      width: 0.3em;
+      height: 0.3em;
+      background-color: #f8bb86;
+      left: 50%;
+      bottom: 0.6em;
+      margin-left: -0.15em;
+      border-radius: 50%;
+    }
+  `;
+
+  // Inject Styles
+  const injectStyles = () => {
+    if (document.getElementById(`${PREFIX}styles`)) return;
+    const styleSheet = document.createElement('style');
+    styleSheet.id = `${PREFIX}styles`;
+    styleSheet.textContent = css;
+    document.head.appendChild(styleSheet);
+  };
+
+  class Layer {
+    constructor() {
+      injectStyles();
+      this.params = {};
+      this.dom = {};
+      this.promise = null;
+      this.resolve = null;
+      this.reject = null;
+    }
+
+    // Constructor helper when called as function: const popup = Layer({...})
+    static get isProxy() { return true; }
+    
+    // Static entry point
+    static fire(options) {
+      const instance = new Layer();
+      return instance._fire(options);
+    }
+    
+    // Chainable entry point (builder-style)
+    // Example:
+    //   Layer.$({ title: 'Hi' }).fire().then(...)
+    //   Layer.$().config({ title: 'Hi' }).fire()
+    static $(options) {
+      const instance = new Layer();
+      if (options !== undefined) instance.config(options);
+      return instance;
+    }
+    
+    // Chainable config helper (does not render until `.fire()` is called)
+    config(options = {}) {
+      // Support the same shorthand as Layer.fire(title, text, icon)
+      if (typeof options === 'string') {
+        options = { title: options };
+        if (arguments[1]) options.text = arguments[1];
+        if (arguments[2]) options.icon = arguments[2];
+      }
+      this.params = { ...(this.params || {}), ...options };
+      return this;
+    }
+    
+    // Instance entry point (chainable)
+    fire(options) {
+      const merged = (options === undefined) ? (this.params || {}) : options;
+      return this._fire(merged);
+    }
+
+    _fire(options = {}) {
+      if (typeof options === 'string') {
+        options = { title: options };
+        if (arguments[1]) options.text = arguments[1];
+        if (arguments[2]) options.icon = arguments[2];
+      }
+
+      this.params = {
+        title: '',
+        text: '',
+        icon: null,
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        showCancelButton: false,
+        confirmButtonColor: '#3085d6',
+        cancelButtonColor: '#aaa',
+        closeOnClickOutside: true,
+        ...options
+      };
+
+      this.promise = new Promise((resolve, reject) => {
+        this.resolve = resolve;
+        this.reject = reject;
+      });
+
+      this._render();
+      return this.promise;
+    }
+
+    _render() {
+      // Remove existing if any
+      const existing = document.querySelector(`.${PREFIX}overlay`);
+      if (existing) existing.remove();
+
+      // Create Overlay
+      this.dom.overlay = document.createElement('div');
+      this.dom.overlay.className = `${PREFIX}overlay`;
+
+      // Create Popup
+      this.dom.popup = document.createElement('div');
+      this.dom.popup.className = `${PREFIX}popup`;
+      this.dom.overlay.appendChild(this.dom.popup);
+
+      // Icon
+      if (this.params.icon) {
+        this.dom.icon = this._createIcon(this.params.icon);
+        this.dom.popup.appendChild(this.dom.icon);
+      }
+
+      // Title
+      if (this.params.title) {
+        this.dom.title = document.createElement('h2');
+        this.dom.title.className = `${PREFIX}title`;
+        this.dom.title.textContent = this.params.title;
+        this.dom.popup.appendChild(this.dom.title);
+      }
+
+      // Content (Text / HTML / Element)
+      if (this.params.text || this.params.html || this.params.content) {
+        this.dom.content = document.createElement('div');
+        this.dom.content.className = `${PREFIX}content`;
+        
+        if (this.params.content) {
+           // DOM Element or Selector
+           let el = this.params.content;
+           if (typeof el === 'string') el = document.querySelector(el);
+           if (el instanceof Element) this.dom.content.appendChild(el); 
+        } else if (this.params.html) {
+           this.dom.content.innerHTML = this.params.html;
+        } else {
+           this.dom.content.textContent = this.params.text;
+        }
+        
+        this.dom.popup.appendChild(this.dom.content);
+      }
+
+      // Actions
+      this.dom.actions = document.createElement('div');
+      this.dom.actions.className = `${PREFIX}actions`;
+      
+      // Cancel Button
+      if (this.params.showCancelButton) {
+        this.dom.cancelBtn = document.createElement('button');
+        this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
+        this.dom.cancelBtn.textContent = this.params.cancelButtonText;
+        this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
+        this.dom.cancelBtn.onclick = () => this._close(false);
+        this.dom.actions.appendChild(this.dom.cancelBtn);
+      }
+
+      // Confirm Button
+      this.dom.confirmBtn = document.createElement('button');
+      this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
+      this.dom.confirmBtn.textContent = this.params.confirmButtonText;
+      this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
+      this.dom.confirmBtn.onclick = () => this._close(true);
+      this.dom.actions.appendChild(this.dom.confirmBtn);
+
+      this.dom.popup.appendChild(this.dom.actions);
+
+      // Event Listeners
+      if (this.params.closeOnClickOutside) {
+        this.dom.overlay.addEventListener('click', (e) => {
+          if (e.target === this.dom.overlay) {
+            this._close(null); // Dismiss
+          }
+        });
+      }
+
+      document.body.appendChild(this.dom.overlay);
+
+      // Animation
+      requestAnimationFrame(() => {
+        this.dom.overlay.classList.add('show');
+      });
+    }
+
+    _createIcon(type) {
+      const icon = document.createElement('div');
+      icon.className = `${PREFIX}icon ${type}`;
+      
+      if (type === 'success') {
+        const tip = document.createElement('div');
+        tip.className = `${PREFIX}success-line-tip`;
+        const long = document.createElement('div');
+        long.className = `${PREFIX}success-line-long`;
+        icon.appendChild(tip);
+        icon.appendChild(long);
+      } else if (type === 'error') {
+        const xMark = document.createElement('span');
+        xMark.className = `${PREFIX}error-x-mark`;
+        const left = document.createElement('span');
+        left.className = `${PREFIX}error-line left`;
+        const right = document.createElement('span');
+        right.className = `${PREFIX}error-line right`;
+        xMark.appendChild(left);
+        xMark.appendChild(right);
+        icon.appendChild(xMark);
+      } else if (type === 'warning') {
+        const body = document.createElement('div');
+        body.className = `${PREFIX}warning-body`;
+        const dot = document.createElement('div');
+        dot.className = `${PREFIX}warning-dot`;
+        icon.appendChild(body);
+        icon.appendChild(dot);
+      }
+      
+      return icon;
+    }
+
+    _close(isConfirmed) {
+      this.dom.overlay.classList.remove('show');
+      setTimeout(() => {
+        if (this.dom.overlay && this.dom.overlay.parentNode) {
+          this.dom.overlay.parentNode.removeChild(this.dom.overlay);
+        }
+      }, 300);
+
+      if (isConfirmed === true) {
+        this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
+      } else if (isConfirmed === false) {
+        this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
+      } else {
+        this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
+      }
+    }
+  }
+
+  return Layer;
+
+})));
+
+
+
+/* --- animal.js --- */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+      (global.animal = factory());
+}(__GLOBAL__, (function () {
+  'use strict';
+
+  // --- Utils ---
+  const isArr = (a) => Array.isArray(a);
+  const isStr = (s) => typeof s === 'string';
+  const isFunc = (f) => typeof f === 'function';
+  const isNil = (v) => v === undefined || v === null;
+  const isSVG = (el) => (typeof SVGElement !== 'undefined') && (el instanceof SVGElement);
+  const isEl = (v) => (typeof Element !== 'undefined') && (v instanceof Element);
+  const clamp01 = (n) => Math.max(0, Math.min(1, n));
+  const toKebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
+  
+  const toArray = (targets) => {
+    if (isArr(targets)) return targets;
+    if (isStr(targets)) {
+      if (typeof document === 'undefined') return [];
+      return Array.from(document.querySelectorAll(targets));
+    }
+    if ((typeof NodeList !== 'undefined') && (targets instanceof NodeList)) return Array.from(targets);
+    if ((typeof Element !== 'undefined') && (targets instanceof Element)) return [targets];
+    if ((typeof Window !== 'undefined') && (targets instanceof Window)) return [targets];
+    // Plain objects (for anime.js-style object tweening)
+    if (!isNil(targets) && (typeof targets === 'object' || isFunc(targets))) return [targets];
+    return [];
+  };
+
+  // Selection helper (chainable, still Array-compatible)
+  const _layerHandlerByEl = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
+  class Selection extends Array {
+    animate(params) { return animate(this, params); }
+    draw(params = {}) { return svgDraw(this, params); }
+    inViewAnimate(params, options = {}) { return inViewAnimate(this, params, options); }
+    // Layer.js plugin-style helper:
+    //   const $ = animal.$;
+    //   $('.btn').layer({ title: 'Hi' });
+    // Clicking the element will open Layer.
+    layer(options) {
+      const LayerCtor =
+        (typeof window !== 'undefined' && window.Layer) ? window.Layer :
+        (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
+        (typeof Layer !== 'undefined' ? Layer : null);
+      if (!LayerCtor) return this;
+      
+      this.forEach((el) => {
+        if (!isEl(el)) return;
+        // De-dupe / replace previous binding
+        if (_layerHandlerByEl) {
+          const prev = _layerHandlerByEl.get(el);
+          if (prev) el.removeEventListener('click', prev);
+        }
+        
+        const handler = () => {
+          let opts = options;
+          if (isFunc(options)) {
+            opts = options(el);
+          } else if (isNil(options)) {
+            opts = null;
+          }
+          // If no explicit options, allow data-* configuration
+          if (!opts || (typeof opts === 'object' && Object.keys(opts).length === 0)) {
+            const d = el.dataset || {};
+            opts = {
+              title: d.layerTitle || el.getAttribute('data-layer-title') || (el.textContent || '').trim(),
+              text: d.layerText || el.getAttribute('data-layer-text') || '',
+              icon: d.layerIcon || el.getAttribute('data-layer-icon') || null,
+              showCancelButton: (d.layerCancel === 'true') || (el.getAttribute('data-layer-cancel') === 'true')
+            };
+          }
+          
+          // Prefer builder API if present, fallback to static fire.
+          if (LayerCtor.$ && isFunc(LayerCtor.$)) return LayerCtor.$(opts).fire();
+          if (LayerCtor.fire && isFunc(LayerCtor.fire)) return LayerCtor.fire(opts);
+        };
+        
+        if (_layerHandlerByEl) _layerHandlerByEl.set(el, handler);
+        el.addEventListener('click', handler);
+      });
+      
+      return this;
+    }
+    
+    // Remove click bindings added by `.layer()`
+    unlayer() {
+      this.forEach((el) => {
+        if (!isEl(el)) return;
+        if (!_layerHandlerByEl) return;
+        const prev = _layerHandlerByEl.get(el);
+        if (prev) el.removeEventListener('click', prev);
+        _layerHandlerByEl.delete(el);
+      });
+      return this;
+    }
+  }
+  
+  const $ = (targets) => Selection.from(toArray(targets));
+
+  const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth'];
+
+  const getUnit = (val, prop) => {
+    if (UNITLESS_KEYS.includes(prop)) return '';
+    const split = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(val);
+    return split ? split[1] : undefined;
+  };
+  
+  const isCssVar = (k) => isStr(k) && k.startsWith('--');
+  // Keep this intentionally broad so "unknown" config keys don't accidentally become animated props.
+  // Prefer putting non-anim-prop config under `params.options`.
+  const isAnimOptionKey = (k) => [
+    'options',
+    'duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay',
+    'update', 'begin', 'complete',
+    // WAAPI-ish common keys (ignored unless we explicitly support them)
+    'iterations', 'iterationStart', 'iterationComposite', 'composite', 'playbackRate',
+    // Spring helpers
+    'springFrames'
+  ].includes(k);
+  
+  const isEasingFn = (e) => typeof e === 'function';
+  
+  // Minimal cubic-bezier implementation (for JS engine easing)
+  function cubicBezier(x1, y1, x2, y2) {
+    // Inspired by https://github.com/gre/bezier-easing (simplified)
+    const NEWTON_ITERATIONS = 4;
+    const NEWTON_MIN_SLOPE = 0.001;
+    const SUBDIVISION_PRECISION = 0.0000001;
+    const SUBDIVISION_MAX_ITERATIONS = 10;
+    
+    const kSplineTableSize = 11;
+    const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
+    
+    const float32ArraySupported = typeof Float32Array === 'function';
+    
+    function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
+    function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
+    function C(aA1) { return 3.0 * aA1; }
+    
+    function calcBezier(aT, aA1, aA2) {
+      return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
+    }
+    
+    function getSlope(aT, aA1, aA2) {
+      return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
+    }
+    
+    function binarySubdivide(aX, aA, aB) {
+      let currentX, currentT, i = 0;
+      do {
+        currentT = aA + (aB - aA) / 2.0;
+        currentX = calcBezier(currentT, x1, x2) - aX;
+        if (currentX > 0.0) aB = currentT;
+        else aA = currentT;
+      } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
+      return currentT;
+    }
+    
+    function newtonRaphsonIterate(aX, aGuessT) {
+      for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
+        const currentSlope = getSlope(aGuessT, x1, x2);
+        if (currentSlope === 0.0) return aGuessT;
+        const currentX = calcBezier(aGuessT, x1, x2) - aX;
+        aGuessT -= currentX / currentSlope;
+      }
+      return aGuessT;
+    }
+    
+    const sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
+    for (let i = 0; i < kSplineTableSize; ++i) {
+      sampleValues[i] = calcBezier(i * kSampleStepSize, x1, x2);
+    }
+    
+    function getTForX(aX) {
+      let intervalStart = 0.0;
+      let currentSample = 1;
+      const lastSample = kSplineTableSize - 1;
+      
+      for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
+        intervalStart += kSampleStepSize;
+      }
+      --currentSample;
+      
+      const dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
+      const guessForT = intervalStart + dist * kSampleStepSize;
+      
+      const initialSlope = getSlope(guessForT, x1, x2);
+      if (initialSlope >= NEWTON_MIN_SLOPE) return newtonRaphsonIterate(aX, guessForT);
+      if (initialSlope === 0.0) return guessForT;
+      return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
+    }
+    
+    return (x) => {
+      if (x === 0 || x === 1) return x;
+      return calcBezier(getTForX(x), y1, y2);
+    };
+  }
+  
+  function resolveJsEasing(easing, springValues) {
+    if (isEasingFn(easing)) return easing;
+    if (typeof easing === 'object' && easing && !Array.isArray(easing)) {
+      // Spring: map linear progress -> simulated spring curve (0..1)
+      const values = springValues || getSpringValues(easing);
+      const last = Math.max(0, values.length - 1);
+      return (t) => {
+        const p = clamp01(t);
+        const idx = p * last;
+        const i0 = Math.floor(idx);
+        const i1 = Math.min(last, i0 + 1);
+        const frac = idx - i0;
+        const v0 = values[i0] ?? p;
+        const v1 = values[i1] ?? p;
+        return v0 + (v1 - v0) * frac;
+      };
+    }
+    if (Array.isArray(easing) && easing.length === 4) {
+      return cubicBezier(easing[0], easing[1], easing[2], easing[3]);
+    }
+    // Common named easings
+    switch (easing) {
+      case 'linear': return (t) => t;
+      case 'ease': return cubicBezier(0.25, 0.1, 0.25, 1);
+      case 'ease-in': return cubicBezier(0.42, 0, 1, 1);
+      case 'ease-out': return cubicBezier(0, 0, 0.58, 1);
+      case 'ease-in-out': return cubicBezier(0.42, 0, 0.58, 1);
+      default: return (t) => t;
+    }
+  }
+
+  // --- Spring Physics (Simplified) ---
+  // Returns an array of [time, value] or just value for WAAPI linear easing
+  function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) {
+    const values = [];
+    let t = 0;
+    const timeStep = 1 / 60; // 60fps simulation
+    let current = 0;
+    let v = velocity;
+    const target = 1;
+
+    let running = true;
+    while (running && t < 10) { // Safety break at 10s
+      const fSpring = -stiffness * (current - target);
+      const fDamper = -damping * v;
+      const a = (fSpring + fDamper) / mass;
+      
+      v += a * timeStep;
+      current += v * timeStep;
+      
+      values.push(current);
+      t += timeStep;
+
+      if (Math.abs(current - target) < precision && Math.abs(v) < precision) {
+        running = false;
+      }
+    }
+    if (values[values.length - 1] !== 1) values.push(1);
+    
+    return values;
+  }
+  
+  // Cache spring curves by config (perf: avoid recomputing for many targets)
+  // LRU-ish capped cache to avoid unbounded growth in long-lived apps.
+  const SPRING_CACHE_MAX = 50;
+  const springCache = new Map();
+  function springCacheGet(key) {
+    const v = springCache.get(key);
+    if (!v) return v;
+    // refresh LRU
+    springCache.delete(key);
+    springCache.set(key, v);
+    return v;
+  }
+  function springCacheSet(key, values) {
+    if (springCache.has(key)) springCache.delete(key);
+    springCache.set(key, values);
+    if (springCache.size > SPRING_CACHE_MAX) {
+      const firstKey = springCache.keys().next().value;
+      if (firstKey !== undefined) springCache.delete(firstKey);
+    }
+  }
+  function getSpringValues(config = {}) {
+    const {
+      stiffness = 100,
+      damping = 10,
+      mass = 1,
+      velocity = 0,
+      precision = 0.01
+    } = config || {};
+    const key = `${stiffness}|${damping}|${mass}|${velocity}|${precision}`;
+    const cached = springCacheGet(key);
+    if (cached) return cached;
+    const values = spring({ stiffness, damping, mass, velocity, precision });
+    springCacheSet(key, values);
+    return values;
+  }
+  
+  function downsample(values, maxFrames = 120) {
+    if (!values || values.length <= maxFrames) return values || [];
+    const out = [];
+    const lastIndex = values.length - 1;
+    const step = lastIndex / (maxFrames - 1);
+    for (let i = 0; i < maxFrames; i++) {
+      const idx = i * step;
+      const i0 = Math.floor(idx);
+      const i1 = Math.min(lastIndex, i0 + 1);
+      const frac = idx - i0;
+      const v0 = values[i0];
+      const v1 = values[i1];
+      out.push(v0 + (v1 - v0) * frac);
+    }
+    if (out[out.length - 1] !== 1) out[out.length - 1] = 1;
+    return out;
+  }
+
+  // --- WAAPI Core ---
+  const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y'];
+  
+  const ALIASES = {
+    x: 'translateX',
+    y: 'translateY',
+    z: 'translateZ'
+  };
+
+  // Basic rAF loop for non-WAAPI props or fallback
+  class RafEngine {
+    constructor() {
+      this.animations = [];
+      this.tick = this.tick.bind(this);
+      this.running = false;
+    }
+    add(anim) {
+      this.animations.push(anim);
+      if (!this.running) {
+        this.running = true;
+        requestAnimationFrame(this.tick);
+      }
+    }
+    remove(anim) {
+      this.animations = this.animations.filter(a => a !== anim);
+    }
+    tick(t) {
+      const now = t;
+      this.animations = this.animations.filter(anim => {
+        return anim.tick(now); // return true to keep
+      });
+      if (this.animations.length) {
+        requestAnimationFrame(this.tick);
+      } else {
+        this.running = false;
+      }
+    }
+  }
+  const rafEngine = new RafEngine();
+  
+  // --- WAAPI update sampler (only used when update callback is provided) ---
+  class WaapiUpdateSampler {
+    constructor() {
+      this.items = new Set();
+      this._running = false;
+      this._tick = this._tick.bind(this);
+    }
+    add(item) {
+      this.items.add(item);
+      if (!this._running) {
+        this._running = true;
+        requestAnimationFrame(this._tick);
+      }
+    }
+    remove(item) {
+      this.items.delete(item);
+    }
+    _tick(t) {
+      if (!this.items.size) {
+        this._running = false;
+        return;
+      }
+      let anyActive = false;
+      let anyChanged = false;
+      this.items.forEach((item) => {
+        const { anim, target, update } = item;
+        if (!anim || !anim.effect) return;
+        let dur = item._dur || 0;
+        if (!dur) {
+          const timing = readWaapiEffectTiming(anim.effect);
+          dur = timing.duration || 0;
+          item._dur = dur;
+        }
+        if (!dur) return;
+        const p = clamp01((anim.currentTime || 0) / dur);
+        if (anim.playState === 'running') anyActive = true;
+        if (item._lastP !== p) {
+          anyChanged = true;
+          item._lastP = p;
+          update({ target, progress: p, time: t });
+        }
+        if (anim.playState === 'finished' || anim.playState === 'idle') {
+          this.items.delete(item);
+        }
+      });
+      if (this.items.size && (anyActive || anyChanged)) {
+        requestAnimationFrame(this._tick);
+      } else {
+        this._running = false;
+      }
+    }
+  }
+  const waapiUpdateSampler = new WaapiUpdateSampler();
+  
+  function readWaapiEffectTiming(effect) {
+    // We compute this once per animation and cache it to avoid per-frame getComputedTiming().
+    // We primarily cache `duration` to preserve existing behavior (progress maps to one iteration).
+    let duration = 0;
+    let endTime = 0;
+    if (!effect) return { duration, endTime };
+    try {
+      const ct = effect.getComputedTiming ? effect.getComputedTiming() : null;
+      if (ct) {
+        if (typeof ct.duration === 'number' && Number.isFinite(ct.duration)) duration = ct.duration;
+        if (typeof ct.endTime === 'number' && Number.isFinite(ct.endTime)) endTime = ct.endTime;
+      }
+    } catch (e) {
+      // ignore
+    }
+    if (!endTime) endTime = duration || 0;
+    return { duration, endTime };
+  }
   
-  // Ensure we have a global entry point if not already
-  const x = global.animal || {};
+  function getDefaultUnit(prop) {
+    if (!prop) return '';
+    if (UNITLESS_KEYS.includes(prop)) return '';
+    if (prop.startsWith('scale')) return '';
+    if (prop === 'opacity') return '';
+    if (prop.startsWith('rotate') || prop.startsWith('skew')) return 'deg';
+    return 'px';
+  }
   
-  // Extend animal with Layer features if not present
-  if (global.Layer && !x.Layer) {
-    x.Layer = global.Layer;
+  function normalizeTransformPartValue(prop, v) {
+    if (typeof v !== 'number') return v;
+    if (prop && prop.startsWith('scale')) return '' + v;
+    if (prop && (prop.startsWith('rotate') || prop.startsWith('skew'))) return v + 'deg';
+    return v + 'px';
+  }
+  
+  // --- JS Interpolator (anime.js-ish, simplified) ---
+  class JsAnimation {
+    constructor(target, propValues, opts = {}, callbacks = {}) {
+      this.target = target;
+      this.propValues = propValues;
+      this.duration = opts.duration ?? 1000;
+      this.delay = opts.delay ?? 0;
+      this.direction = opts.direction ?? 'normal';
+      this.loop = opts.loop ?? 1;
+      this.endDelay = opts.endDelay ?? 0;
+      this.easing = opts.easing ?? 'linear';
+      this.autoplay = opts.autoplay !== false;
+      
+      this.update = callbacks.update;
+      this.begin = callbacks.begin;
+      this.complete = callbacks.complete;
+      
+      this._resolve = null;
+      this.finished = new Promise((res) => (this._resolve = res));
+      
+      this._started = false;
+      this._running = false;
+      this._paused = false;
+      this._cancelled = false;
+      this._startTime = 0;
+      this._progress = 0;
+      this._didBegin = false;
+      
+      this._ease = resolveJsEasing(this.easing, opts.springValues);
+      
+      this._tween = this._buildTween();
+      this.tick = this.tick.bind(this);
+      
+      if (this.autoplay) this.play();
+    }
+    
+    _readCurrentValue(k) {
+      const t = this.target;
+      // JS object
+      if (!isEl(t) && !isSVG(t)) return t[k];
+      
+      // scroll
+      if (k === 'scrollTop' || k === 'scrollLeft') return t[k];
+      
+      // CSS var
+      if (isEl(t) && isCssVar(k)) {
+        const cs = getComputedStyle(t);
+        return (cs.getPropertyValue(k) || t.style.getPropertyValue(k) || '').trim();
+      }
+      
+      // style
+      if (isEl(t)) {
+        const cs = getComputedStyle(t);
+        // Prefer computed style (kebab) because many props aren't direct keys on cs
+        const v = cs.getPropertyValue(toKebab(k));
+        if (v && v.trim()) return v.trim();
+        // Fallback to inline style access
+        if (k in t.style) return t.style[k];
+      }
+      
+      // SVG attribute
+      if (isSVG(t)) {
+        const attr = toKebab(k);
+        if (t.hasAttribute(attr)) return t.getAttribute(attr);
+        if (t.hasAttribute(k)) return t.getAttribute(k);
+      }
+      
+      // Generic property
+      return t[k];
+    }
+    
+    _writeValue(k, v) {
+      const t = this.target;
+      // JS object
+      if (!isEl(t) && !isSVG(t)) {
+        t[k] = v;
+        return;
+      }
+      
+      // scroll
+      if (k === 'scrollTop' || k === 'scrollLeft') {
+        t[k] = v;
+        return;
+      }
+      
+      // CSS var
+      if (isEl(t) && isCssVar(k)) {
+        t.style.setProperty(k, v);
+        return;
+      }
+      
+      // style
+      if (isEl(t) && (k in t.style)) {
+        t.style[k] = v;
+        return;
+      }
+      
+      // SVG attribute
+      if (isSVG(t)) {
+        const attr = toKebab(k);
+        t.setAttribute(attr, v);
+        return;
+      }
+      
+      // Generic property
+      t[k] = v;
+    }
+    
+    _buildTween() {
+      const tween = {};
+      Object.keys(this.propValues).forEach((k) => {
+        const raw = this.propValues[k];
+        const fromRaw = isArr(raw) ? raw[0] : this._readCurrentValue(k);
+        const toRaw = isArr(raw) ? raw[1] : raw;
+        
+        const fromStr = isNil(fromRaw) ? '0' : ('' + fromRaw).trim();
+        const toStr = isNil(toRaw) ? '0' : ('' + toRaw).trim();
+        
+        const fromNum = parseFloat(fromStr);
+        const toNum = parseFloat(toStr);
+        const fromNumOk = !Number.isNaN(fromNum);
+        const toNumOk = !Number.isNaN(toNum);
+        
+        // numeric tween (with unit preservation)
+        if (fromNumOk && toNumOk) {
+          const unit = getUnit(toStr, k) ?? getUnit(fromStr, k) ?? '';
+          tween[k] = { type: 'number', from: fromNum, to: toNum, unit };
+        } else {
+          // Non-numeric: fall back to "switch" (still useful for seek endpoints)
+          tween[k] = { type: 'discrete', from: fromStr, to: toStr };
+        }
+      });
+      return tween;
+    }
+    
+    _apply(progress, time) {
+      this._progress = clamp01(progress);
+      Object.keys(this._tween).forEach((k) => {
+        const t = this._tween[k];
+        if (t.type === 'number') {
+          const eased = this._ease ? this._ease(this._progress) : this._progress;
+          const val = t.from + (t.to - t.from) * eased;
+          if (!isEl(this.target) && !isSVG(this.target) && t.unit === '') {
+            this._writeValue(k, val);
+          } else {
+            this._writeValue(k, (val + t.unit));
+          }
+        } else {
+          const val = this._progress >= 1 ? t.to : t.from;
+          this._writeValue(k, val);
+        }
+      });
+      
+      if (this.update) this.update({ target: this.target, progress: this._progress, time });
+    }
+    
+    seek(progress) {
+      // Seek does not auto-play; it's intended for scroll-linked or manual control.
+      const t = (typeof performance !== 'undefined' ? performance.now() : 0);
+      this._apply(progress, t);
+    }
+    
+    play() {
+      if (this._cancelled) return;
+      if (!this._started) {
+        this._started = true;
+        this._startTime = performance.now() + this.delay - (this._progress * this.duration);
+        // begin fired on first active tick to avoid firing during delay.
+      }
+      this._paused = false;
+      if (!this._running) {
+        this._running = true;
+        rafEngine.add(this);
+      }
+    }
+    
+    pause() {
+      this._paused = true;
+    }
+    
+    cancel() {
+      this._cancelled = true;
+      this._running = false;
+      rafEngine.remove(this);
+      // Resolve to avoid hanging awaits
+      if (this._resolve) this._resolve();
+    }
+    
+    finish() {
+      this.seek(1);
+      this._running = false;
+      rafEngine.remove(this);
+      if (this.complete) this.complete(this.target);
+      if (this._resolve) this._resolve();
+    }
+    
+    tick(now) {
+      if (this._cancelled) return false;
+      if (this._paused) return true;
+      
+      if (!this._started) {
+        this._started = true;
+        this._startTime = now + this.delay;
+      }
+      
+      if (now < this._startTime) return true;
+      if (!this._didBegin) {
+        this._didBegin = true;
+        if (this.begin) this.begin(this.target);
+      }
+      
+      const totalDur = this.duration + (this.endDelay || 0);
+      const elapsed = now - this._startTime;
+      const iter = totalDur > 0 ? Math.floor(elapsed / totalDur) : 0;
+      const inIter = totalDur > 0 ? (elapsed - iter * totalDur) : elapsed;
+      
+      const iterations = this.loop === true ? Infinity : this.loop;
+      if (iterations !== Infinity && iter >= iterations) {
+        this._apply(this._mapDirection(1, iterations - 1));
+        this._running = false;
+        if (this.complete) this.complete(this.target);
+        if (this._resolve) this._resolve();
+        return false;
+      }
+      
+      // if we're in endDelay portion, hold the end state
+      let p = clamp01(inIter / this.duration);
+      if (this.duration <= 0) p = 1;
+      if (this.endDelay && inIter > this.duration) p = 1;
+      
+      this._apply(this._mapDirection(p, iter), now);
+      
+      // Keep running until loops exhausted
+      return true;
+    }
+    
+    _mapDirection(p, iterIndex) {
+      const dir = this.direction;
+      const flip = (dir === 'reverse') || (dir === 'alternate-reverse');
+      const isAlt = (dir === 'alternate') || (dir === 'alternate-reverse');
+      let t = flip ? (1 - p) : p;
+      if (isAlt && (iterIndex % 2 === 1)) t = 1 - t;
+      return t;
+    }
+  }
+  
+  // --- Controls (Motion One-ish, chainable / thenable) ---
+  class Controls {
+    constructor({ waapi = [], js = [], finished }) {
+      this.animations = waapi; // backward compat with old `.animations` usage
+      this.jsAnimations = js;
+      this.finished = finished || Promise.resolve();
+    }
+    then(onFulfilled, onRejected) { return this.finished.then(onFulfilled, onRejected); }
+    catch(onRejected) { return this.finished.catch(onRejected); }
+    finally(onFinally) { return this.finished.finally(onFinally); }
+    
+    play() {
+      if (this._onPlay) this._onPlay.forEach((fn) => fn && fn());
+      if (this._ensureWaapiUpdate) this._ensureWaapiUpdate();
+      this.animations.forEach((a) => a && a.play && a.play());
+      this.jsAnimations.forEach((a) => a && a.play && a.play());
+      return this;
+    }
+    pause() {
+      this.animations.forEach((a) => a && a.pause && a.pause());
+      this.jsAnimations.forEach((a) => a && a.pause && a.pause());
+      return this;
+    }
+    cancel() {
+      this.animations.forEach((a) => a && a.cancel && a.cancel());
+      this.jsAnimations.forEach((a) => a && a.cancel && a.cancel());
+      return this;
+    }
+    finish() {
+      this.animations.forEach((a) => a && a.finish && a.finish());
+      this.jsAnimations.forEach((a) => a && a.finish && a.finish());
+      return this;
+    }
+    seek(progress) {
+      const p = clamp01(progress);
+      const t = (typeof performance !== 'undefined' ? performance.now() : 0);
+      this.animations.forEach((anim) => {
+        if (anim && anim.effect) {
+          let timing = this._waapiTimingByAnim && this._waapiTimingByAnim.get(anim);
+          if (!timing) {
+            timing = readWaapiEffectTiming(anim.effect);
+            if (this._waapiTimingByAnim) this._waapiTimingByAnim.set(anim, timing);
+          }
+          const dur = timing.duration || 0;
+          if (!dur) return;
+          anim.currentTime = dur * p;
+          const target = this._waapiTargetByAnim && this._waapiTargetByAnim.get(anim);
+          if (this._fireUpdate && target) this._fireUpdate({ target, progress: p, time: t });
+        }
+      });
+      this.jsAnimations.forEach((a) => a && a.seek && a.seek(p));
+      return this;
+    }
+  }
+
+  // --- Main Animation Logic ---
+  function animate(targets, params) {
+    const elements = toArray(targets);
+    const safeParams = params || {};
+    const optionsNamespace = (safeParams.options && typeof safeParams.options === 'object') ? safeParams.options : {};
+    // `params.options` provides a safe namespace for config keys without risking them being treated as animated props.
+    // Top-level keys still win for backward compatibility.
+    const merged = { ...optionsNamespace, ...safeParams };
+    const { 
+      duration = 1000, 
+      delay = 0, 
+      easing = 'ease-out', 
+      direction = 'normal', 
+      fill = 'forwards',
+      loop = 1,
+      endDelay = 0,
+      autoplay = true,
+      springFrames = 120,
+      update, // callback
+      begin, // callback
+      complete // callback
+    } = merged;
+
+    let isSpring = false;
+    let springValuesRaw = null;
+    let springValuesSampled = null;
+    let springDurationMs = null;
+
+    if (typeof easing === 'object' && !Array.isArray(easing)) {
+        isSpring = true;
+        springValuesRaw = getSpringValues(easing);
+        const frames = (typeof springFrames === 'number' && springFrames > 1) ? Math.floor(springFrames) : 120;
+        springValuesSampled = downsample(springValuesRaw, frames);
+        springDurationMs = springValuesRaw.length * 1000 / 60;
+    }
+    
+    // Callback aggregation (avoid double-calling when WAAPI+JS both run)
+    const cbState = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
+    const getState = (t) => {
+      if (!cbState) return { begun: false, completed: false, lastUpdateBucket: -1 };
+      let s = cbState.get(t);
+      if (!s) {
+        s = { begun: false, completed: false, lastUpdateBucket: -1 };
+        cbState.set(t, s);
+      }
+      return s;
+    };
+    const fireBegin = (t) => {
+      if (!begin) return;
+      const s = getState(t);
+      if (s.begun) return;
+      s.begun = true;
+      begin(t);
+    };
+    const fireComplete = (t) => {
+      if (!complete) return;
+      const s = getState(t);
+      if (s.completed) return;
+      s.completed = true;
+      complete(t);
+    };
+    const fireUpdate = (payload) => {
+      if (!update) return;
+      const t = payload && payload.target;
+      const time = payload && payload.time;
+      if (!t) return update(payload);
+      const s = getState(t);
+      const bucket = Math.floor(((typeof time === 'number' ? time : (typeof performance !== 'undefined' ? performance.now() : 0))) / 16);
+      if (bucket === s.lastUpdateBucket) return;
+      s.lastUpdateBucket = bucket;
+      update(payload);
+    };
+
+    const propEntries = [];
+    Object.keys(safeParams).forEach((key) => {
+      if (key === 'options') return;
+      if (isAnimOptionKey(key)) return;
+      propEntries.push({ key, canonical: ALIASES[key] || key, val: safeParams[key] });
+    });
+
+    // Create animations but don't play if autoplay is false
+    const waapiAnimations = [];
+    const jsAnimations = [];
+    const engineInfo = {
+      isSpring,
+      waapiKeys: [],
+      jsKeys: []
+    };
+    const waapiKeySet = new Set();
+    const jsKeySet = new Set();
+    
+    const onPlayHooks = [];
+    const waapiUpdateItems = [];
+    const waapiTargetByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
+    const waapiTimingByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
+    let waapiUpdateStarted = false;
+    const ensureWaapiUpdate = () => {
+      if (waapiUpdateStarted) return;
+      waapiUpdateStarted = true;
+      waapiUpdateItems.forEach((item) => waapiUpdateSampler.add(item));
+    };
+
+    const promises = elements.map((el) => {
+      // Route props per target (fix mixed HTML/SVG/object target arrays).
+      const waapiProps = {};
+      const jsProps = {};
+      propEntries.forEach(({ key, canonical, val }) => {
+        const isTransform = TRANSFORMS.includes(canonical) || TRANSFORMS.includes(key);
+        if (!el || (!isEl(el) && !isSVG(el))) {
+          jsProps[key] = val;
+          jsKeySet.add(key);
+          return;
+        }
+        const isSvgTarget = isSVG(el);
+        if (isSvgTarget) {
+          if (isTransform || key === 'opacity' || key === 'filter') {
+            waapiProps[canonical] = val;
+            waapiKeySet.add(canonical);
+          } else {
+            jsProps[key] = val;
+            jsKeySet.add(key);
+          }
+          return;
+        }
+        // HTML element
+        if (isCssVar(key)) {
+          jsProps[key] = val;
+          jsKeySet.add(key);
+          return;
+        }
+        const isCssLike = isTransform || key === 'opacity' || key === 'filter' || (isEl(el) && (key in el.style));
+        if (isCssLike) {
+          waapiProps[canonical] = val;
+          waapiKeySet.add(canonical);
+        } else {
+          jsProps[key] = val;
+          jsKeySet.add(key);
+        }
+      });
+
+      // 1. WAAPI Animation
+      let waapiAnim = null;
+      let waapiPromise = Promise.resolve();
+      
+      if (Object.keys(waapiProps).length > 0) {
+        const buildFrames = (propValues) => {
+          // Spring: share sampled progress; per-target we only compute start/end once per prop.
+          if (isSpring && springValuesSampled && springValuesSampled.length) {
+            const cs = (isEl(el) && typeof getComputedStyle !== 'undefined') ? getComputedStyle(el) : null;
+            const metas = Object.keys(propValues).map((k) => {
+              const raw = propValues[k];
+              const rawFrom = Array.isArray(raw) ? raw[0] : undefined;
+              const rawTo = Array.isArray(raw) ? raw[1] : raw;
+              const toNum = parseFloat(('' + rawTo).trim());
+              const fromNumExplicit = Array.isArray(raw) ? parseFloat(('' + rawFrom).trim()) : NaN;
+              const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
+              const unit = getUnit(('' + rawTo), k) ?? getUnit(('' + rawFrom), k) ?? getDefaultUnit(k);
+              
+              let fromNum = 0;
+              if (Number.isFinite(fromNumExplicit)) {
+                fromNum = fromNumExplicit;
+              } else if (k.startsWith('scale')) {
+                fromNum = 1;
+              } else if (k === 'opacity' && cs) {
+                const n = parseFloat(cs.opacity);
+                if (!Number.isNaN(n)) fromNum = n;
+              } else if (!isT && cs) {
+                const cssVal = cs.getPropertyValue(toKebab(k));
+                const n = parseFloat(cssVal);
+                if (!Number.isNaN(n)) fromNum = n;
+              }
+              
+              const to = Number.isFinite(toNum) ? toNum : 0;
+              return { k, isT, unit: unit ?? '', from: fromNum, to };
+            });
+            
+            const frames = new Array(springValuesSampled.length);
+            for (let i = 0; i < springValuesSampled.length; i++) {
+              const v = springValuesSampled[i];
+              const frame = {};
+              let transformStr = '';
+              for (let j = 0; j < metas.length; j++) {
+                const m = metas[j];
+                const current = m.from + (m.to - m.from) * v;
+                const outVal = (m.unit === '' ? ('' + current) : (current + m.unit));
+                if (m.isT) transformStr += `${m.k}(${outVal}) `;
+                else frame[m.k] = outVal;
+              }
+              if (transformStr) frame.transform = transformStr.trim();
+              frames[i] = frame;
+            }
+            if (frames[0] && Object.keys(frames[0]).length === 0) frames.shift();
+            return frames;
+          }
+          
+          // Non-spring: 2-keyframe path
+          const frame0 = {};
+          const frame1 = {};
+          let transform0 = '';
+          let transform1 = '';
+          Object.keys(propValues).forEach((k) => {
+            const val = propValues[k];
+            const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
+            if (Array.isArray(val)) {
+              const from = isT ? normalizeTransformPartValue(k, val[0]) : val[0];
+              const to = isT ? normalizeTransformPartValue(k, val[1]) : val[1];
+              if (isT) {
+                transform0 += `${k}(${from}) `;
+                transform1 += `${k}(${to}) `;
+              } else {
+                frame0[k] = from;
+                frame1[k] = to;
+              }
+            } else {
+              if (isT) transform1 += `${k}(${normalizeTransformPartValue(k, val)}) `;
+              else frame1[k] = val;
+            }
+          });
+          if (transform0) frame0.transform = transform0.trim();
+          if (transform1) frame1.transform = transform1.trim();
+          const out = [frame0, frame1];
+          if (Object.keys(out[0]).length === 0) out.shift();
+          return out;
+        };
+
+        const finalFrames = buildFrames(waapiProps);
+
+        const opts = {
+            duration: isSpring ? springDurationMs : duration,
+            delay,
+            fill,
+            iterations: loop,
+            easing: isSpring ? 'linear' : easing,
+            direction,
+            endDelay
+        };
+
+        const animation = el.animate(finalFrames, opts);
+        if (!autoplay) animation.pause();
+        waapiAnim = animation;
+        waapiPromise = animation.finished;
+        waapiAnimations.push(waapiAnim);
+        if (waapiTargetByAnim) waapiTargetByAnim.set(waapiAnim, el);
+        if (waapiTimingByAnim) waapiTimingByAnim.set(waapiAnim, readWaapiEffectTiming(waapiAnim.effect));
+        
+        if (begin) {
+          // Fire begin when play starts (and also for autoplay on next frame)
+          onPlayHooks.push(() => fireBegin(el));
+          if (autoplay && typeof requestAnimationFrame !== 'undefined') requestAnimationFrame(() => fireBegin(el));
+        }
+        if (complete) {
+          waapiAnim.addEventListener?.('finish', () => fireComplete(el));
+        }
+        if (update) {
+          const timing = (waapiTimingByAnim && waapiTimingByAnim.get(waapiAnim)) || readWaapiEffectTiming(waapiAnim.effect);
+          const item = { anim: waapiAnim, target: el, update: fireUpdate, _lastP: null, _dur: timing.duration || 0 };
+          waapiUpdateItems.push(item);
+          // Ensure removal on finish/cancel
+          waapiAnim.addEventListener?.('finish', () => waapiUpdateSampler.remove(item));
+          waapiAnim.addEventListener?.('cancel', () => waapiUpdateSampler.remove(item));
+          // Start sampler only when needed (autoplay or explicit play)
+          if (autoplay) ensureWaapiUpdate();
+        }
+      }
+
+      // 2. JS Animation (Fallback / Attributes)
+      let jsPromise = Promise.resolve();
+      if (Object.keys(jsProps).length > 0) {
+        const jsAnim = new JsAnimation(
+          el,
+          jsProps,
+          { duration, delay, easing, autoplay, direction, loop, endDelay, springValues: isSpring ? springValuesRaw : null },
+          { update: fireUpdate, begin: fireBegin, complete: fireComplete }
+        );
+        jsAnimations.push(jsAnim);
+        jsPromise = jsAnim.finished;
+      }
+
+      return Promise.all([waapiPromise, jsPromise]);
+    });
+
+    const finished = Promise.all(promises);
+    const controls = new Controls({ waapi: waapiAnimations, js: jsAnimations, finished });
+    controls.engine = engineInfo;
+    controls._onPlay = onPlayHooks;
+    controls._fireUpdate = fireUpdate;
+    controls._waapiTargetByAnim = waapiTargetByAnim;
+    controls._waapiTimingByAnim = waapiTimingByAnim;
+    controls._ensureWaapiUpdate = update ? ensureWaapiUpdate : null;
+    if (!autoplay) controls.pause();
+    engineInfo.waapiKeys = Array.from(waapiKeySet);
+    engineInfo.jsKeys = Array.from(jsKeySet);
+    return controls;
+  }
+
+  // --- SVG Draw ---
+  function svgDraw(targets, params = {}) {
+     const elements = toArray(targets);
+     elements.forEach(el => {
+         if (!isSVG(el)) return;
+         const len = el.getTotalLength ? el.getTotalLength() : 0;
+         el.style.strokeDasharray = len;
+         el.style.strokeDashoffset = len; 
+         
+         animate(el, {
+             strokeDashoffset: [len, 0],
+             ...params
+         });
+     });
+  }
+
+  // --- In View ---
+  function inViewAnimate(targets, params, options = {}) {
+      const elements = toArray(targets);
+      const observer = new IntersectionObserver((entries) => {
+          entries.forEach(entry => {
+              if (entry.isIntersecting) {
+                  animate(entry.target, params);
+                  if (options.once !== false) observer.unobserve(entry.target);
+              }
+          });
+      }, { threshold: options.threshold || 0.1 });
+      
+      elements.forEach(el => observer.observe(el));
+      // Return cleanup so callers can disconnect observers in long-lived pages (esp. once:false).
+      return () => {
+        try {
+          elements.forEach((el) => observer.unobserve(el));
+          observer.disconnect();
+        } catch (e) {
+          // ignore
+        }
+      };
   }
   
-  // Export to global 'x' or 'xjs' as short alias?
-  // global.x = x; 
+  // --- Scroll Linked ---
+  function scroll(animationPromise, options = {}) {
+      // options: container (default window), range [start, end] (default viewport logic)
+      const container = options.container || window;
+      const target = options.target || document.body; // Element to track for progress
+      // If passing an animation promise, we control its WAAPI animations
+      
+      const controls = animationPromise;
+      // Back-compat: old return value was a Promise with `.animations`
+      const hasSeek = controls && isFunc(controls.seek);
+      const anims = (controls && controls.animations) || (animationPromise && animationPromise.animations) || [];
+      const jsAnims = (controls && controls.jsAnimations) || [];
+      if (!hasSeek && !anims.length && !jsAnims.length) return;
+      
+      // Cache WAAPI timing per animation to avoid repeated getComputedTiming() during scroll.
+      const timingCache = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
+      const getEndTime = (anim) => {
+        if (!anim || !anim.effect) return 0;
+        if (timingCache) {
+          const cached = timingCache.get(anim);
+          if (cached) return cached;
+        }
+        const timing = readWaapiEffectTiming(anim.effect);
+        const end = timing.duration || 0;
+        if (timingCache && end) timingCache.set(anim, end);
+        return end;
+      };
+
+      const updateScroll = () => {
+          let progress = 0;
+          
+          if (container === window) {
+              const scrollY = window.scrollY;
+              const winH = window.innerHeight;
+              const docH = document.body.scrollHeight;
+              
+              // Simple progress: how far down the page (0 to 1)
+              // Or element based?
+              // Motion One defaults to element entering view.
+              
+              if (options.target) {
+                  const rect = options.target.getBoundingClientRect();
+                  const start = winH; 
+                  const end = -rect.height;
+                  // progress 0 when rect.top == start (just entering)
+                  // progress 1 when rect.top == end (just left)
+                  
+                  const totalDistance = start - end;
+                  const currentDistance = start - rect.top;
+                  progress = currentDistance / totalDistance;
+              } else {
+                  // Whole page scroll
+                  progress = scrollY / (docH - winH);
+              }
+          } else if (container && (typeof Element !== 'undefined') && (container instanceof Element)) {
+              // Scroll container progress
+              const el = container;
+              const scrollTop = el.scrollTop;
+              const max = (el.scrollHeight - el.clientHeight) || 1;
+              if (options.target) {
+                const containerRect = el.getBoundingClientRect();
+                const rect = options.target.getBoundingClientRect();
+                const start = containerRect.height;
+                const end = -rect.height;
+                const totalDistance = start - end;
+                const currentDistance = start - (rect.top - containerRect.top);
+                progress = currentDistance / totalDistance;
+              } else {
+                progress = scrollTop / max;
+              }
+          }
+          
+          // Clamp
+          progress = clamp01(progress);
+          
+          if (hasSeek) {
+            controls.seek(progress);
+            return;
+          }
+          
+          anims.forEach((anim) => {
+            if (anim.effect) {
+              const end = getEndTime(anim);
+              if (!end) return;
+              anim.currentTime = end * progress;
+            }
+          });
+      };
+
+      let rafId = 0;
+      const onScroll = () => {
+        if (rafId) return;
+        rafId = requestAnimationFrame(() => {
+          rafId = 0;
+          updateScroll();
+        });
+      };
+      
+      const eventTarget = (container && container.addEventListener) ? container : window;
+      eventTarget.addEventListener('scroll', onScroll, { passive: true });
+      updateScroll(); // Initial
+      
+      return () => {
+        if (rafId) cancelAnimationFrame(rafId);
+        eventTarget.removeEventListener('scroll', onScroll);
+      };
+  }
+
+  // --- Timeline ---
+  function timeline(defaults = {}) {
+      const steps = [];
+      const api = {
+        currentTime: 0,
+        add: (targets, params, offset) => {
+          const animParams = { ...defaults, ...params };
+          let start = api.currentTime;
+          
+          if (offset !== undefined) {
+            if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2));
+            else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2));
+            else if (typeof offset === 'number') start = offset;
+          }
+          
+          const dur = animParams.duration || 1000;
+          const step = { targets, animParams, start, _scheduled: false };
+          steps.push(step);
+          
+          // Backward compatible: schedule immediately (existing docs rely on this)
+          if (start <= 0) {
+            animate(targets, animParams);
+            step._scheduled = true;
+          } else {
+            setTimeout(() => {
+              animate(targets, animParams);
+            }, start);
+            step._scheduled = true;
+          }
+          
+          api.currentTime = Math.max(api.currentTime, start + dur);
+          return api;
+        },
+        // Optional: if you create a timeline and want to defer scheduling yourself
+        play: () => {
+          steps.forEach((s) => {
+            if (s._scheduled) return;
+            if (s.start <= 0) animate(s.targets, s.animParams);
+            else setTimeout(() => animate(s.targets, s.animParams), s.start);
+            s._scheduled = true;
+          });
+          return api;
+        }
+      };
+      return api;
+  }
+
+  // --- Export ---
+  // transform `$` to be the main export `animal`, with statics attached
+  const animal = $;
   
-})(typeof window !== 'undefined' ? window : this);
+  // Extend $ behavior to act as a global selector and property accessor
+  Object.assign(animal, {
+    animate,
+    timeline,
+    draw: svgDraw,
+    svgDraw,
+    inViewAnimate,
+    spring,
+    scroll,
+    $: animal // Self-reference for backward compatibility
+  });
+
+  // Expose Layer if available or allow lazy loading
+  Object.defineProperty(animal, 'Layer', {
+    get: () => {
+      return (typeof window !== 'undefined' && window.Layer) ? window.Layer :
+             (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
+             (typeof Layer !== 'undefined' ? Layer : null);
+    }
+  });
+
+  // Shortcut for Layer.fire or new Layer()
+  // Allows $.fire({ title: 'Hi' }) or $.layer({ title: 'Hi' })
+  animal.fire = (options) => {
+    const L = animal.Layer;
+    if (L) return (L.fire ? L.fire(options) : new L(options).fire());
+    console.warn('Layer module not loaded.');
+    return Promise.reject('Layer module not loaded');
+  };
+  // 'layer' alias for static usage
+  animal.layer = animal.fire;
+
+  return animal;
+
+})));
+
+
+  // Unified entrypoint
+  if (__GLOBAL__ && __GLOBAL__.animal) {
+    __GLOBAL__.xjs = __GLOBAL__.animal;
+  }
 
+  // Optional jQuery-like alias (opt-in, and won't clobber existing $)
+  try {
+    if (__GLOBAL__ && __GLOBAL__.XJS_GLOBAL_DOLLAR === true && !__GLOBAL__.$ && __GLOBAL__.xjs) {
+      __GLOBAL__.$ = __GLOBAL__.xjs;
+    }
+  } catch (_) {}
+})();