@moni-labs/moni-ui 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/custom-elements.json +5230 -1007
  2. package/dist/components/_base/moni-element.d.ts +16 -9
  3. package/dist/components/_base/moni-element.d.ts.map +1 -1
  4. package/dist/components/_base/moni-element.js +63 -9
  5. package/dist/components/index.js +2 -2
  6. package/dist/components/loading-shapes.d.ts +19 -0
  7. package/dist/components/loading-shapes.d.ts.map +1 -1
  8. package/dist/components/loading-shapes.js +19 -0
  9. package/dist/components/moni-app-bar.d.ts +72 -62
  10. package/dist/components/moni-app-bar.d.ts.map +1 -1
  11. package/dist/components/moni-app-bar.js +72 -62
  12. package/dist/components/moni-badge.d.ts +64 -65
  13. package/dist/components/moni-badge.d.ts.map +1 -1
  14. package/dist/components/moni-badge.js +64 -66
  15. package/dist/components/moni-bottom-sheet.d.ts +129 -61
  16. package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
  17. package/dist/components/moni-bottom-sheet.js +217 -131
  18. package/dist/components/moni-button-group.d.ts +107 -29
  19. package/dist/components/moni-button-group.d.ts.map +1 -1
  20. package/dist/components/moni-button-group.js +107 -29
  21. package/dist/components/moni-button-segment.d.ts +53 -12
  22. package/dist/components/moni-button-segment.d.ts.map +1 -1
  23. package/dist/components/moni-button-segment.js +53 -12
  24. package/dist/components/moni-button.d.ts +116 -38
  25. package/dist/components/moni-button.d.ts.map +1 -1
  26. package/dist/components/moni-button.js +128 -38
  27. package/dist/components/moni-card.d.ts +68 -50
  28. package/dist/components/moni-card.d.ts.map +1 -1
  29. package/dist/components/moni-card.js +68 -50
  30. package/dist/components/moni-carousel.d.ts +484 -53
  31. package/dist/components/moni-carousel.d.ts.map +1 -1
  32. package/dist/components/moni-carousel.js +1024 -245
  33. package/dist/components/moni-checkbox.d.ts +49 -46
  34. package/dist/components/moni-checkbox.d.ts.map +1 -1
  35. package/dist/components/moni-checkbox.js +55 -48
  36. package/dist/components/moni-chip.d.ts +100 -33
  37. package/dist/components/moni-chip.d.ts.map +1 -1
  38. package/dist/components/moni-chip.js +104 -34
  39. package/dist/components/moni-color-field.d.ts +99 -24
  40. package/dist/components/moni-color-field.d.ts.map +1 -1
  41. package/dist/components/moni-color-field.js +99 -24
  42. package/dist/components/moni-context-menu.d.ts +72 -26
  43. package/dist/components/moni-context-menu.d.ts.map +1 -1
  44. package/dist/components/moni-context-menu.js +76 -30
  45. package/dist/components/moni-dialog.d.ts +84 -59
  46. package/dist/components/moni-dialog.d.ts.map +1 -1
  47. package/dist/components/moni-dialog.js +105 -58
  48. package/dist/components/moni-divider.d.ts +39 -27
  49. package/dist/components/moni-divider.d.ts.map +1 -1
  50. package/dist/components/moni-divider.js +42 -30
  51. package/dist/components/moni-expansion.d.ts +32 -22
  52. package/dist/components/moni-expansion.d.ts.map +1 -1
  53. package/dist/components/moni-expansion.js +32 -22
  54. package/dist/components/moni-fab-menu.d.ts +61 -23
  55. package/dist/components/moni-fab-menu.d.ts.map +1 -1
  56. package/dist/components/moni-fab-menu.js +69 -31
  57. package/dist/components/moni-fab.d.ts +86 -28
  58. package/dist/components/moni-fab.d.ts.map +1 -1
  59. package/dist/components/moni-fab.js +92 -34
  60. package/dist/components/moni-file-field.d.ts +118 -30
  61. package/dist/components/moni-file-field.d.ts.map +1 -1
  62. package/dist/components/moni-file-field.js +118 -30
  63. package/dist/components/moni-icon.d.ts +40 -35
  64. package/dist/components/moni-icon.d.ts.map +1 -1
  65. package/dist/components/moni-icon.js +43 -38
  66. package/dist/components/moni-list-item.d.ts +46 -33
  67. package/dist/components/moni-list-item.d.ts.map +1 -1
  68. package/dist/components/moni-list-item.js +61 -41
  69. package/dist/components/moni-list.d.ts +30 -20
  70. package/dist/components/moni-list.d.ts.map +1 -1
  71. package/dist/components/moni-list.js +36 -21
  72. package/dist/components/moni-loading-indicator.d.ts +54 -22
  73. package/dist/components/moni-loading-indicator.d.ts.map +1 -1
  74. package/dist/components/moni-loading-indicator.js +54 -22
  75. package/dist/components/moni-menu-item.d.ts +23 -16
  76. package/dist/components/moni-menu-item.d.ts.map +1 -1
  77. package/dist/components/moni-menu-item.js +23 -16
  78. package/dist/components/moni-menu.d.ts +81 -28
  79. package/dist/components/moni-menu.d.ts.map +1 -1
  80. package/dist/components/moni-menu.js +81 -30
  81. package/dist/components/moni-morph-modal.d.ts +141 -0
  82. package/dist/components/moni-morph-modal.d.ts.map +1 -1
  83. package/dist/components/moni-morph-modal.js +295 -58
  84. package/dist/components/moni-nav-item.d.ts +59 -29
  85. package/dist/components/moni-nav-item.d.ts.map +1 -1
  86. package/dist/components/moni-nav-item.js +59 -29
  87. package/dist/components/moni-nav.d.ts +94 -35
  88. package/dist/components/moni-nav.d.ts.map +1 -1
  89. package/dist/components/moni-nav.js +94 -35
  90. package/dist/components/moni-progress.d.ts +99 -55
  91. package/dist/components/moni-progress.d.ts.map +1 -1
  92. package/dist/components/moni-progress.js +99 -55
  93. package/dist/components/moni-radio.d.ts +62 -50
  94. package/dist/components/moni-radio.d.ts.map +1 -1
  95. package/dist/components/moni-radio.js +65 -52
  96. package/dist/components/moni-ripple.d.ts +74 -59
  97. package/dist/components/moni-ripple.d.ts.map +1 -1
  98. package/dist/components/moni-ripple.js +74 -59
  99. package/dist/components/moni-segmented-button.d.ts +91 -16
  100. package/dist/components/moni-segmented-button.d.ts.map +1 -1
  101. package/dist/components/moni-segmented-button.js +94 -17
  102. package/dist/components/moni-select-option.d.ts +30 -23
  103. package/dist/components/moni-select-option.d.ts.map +1 -1
  104. package/dist/components/moni-select-option.js +30 -23
  105. package/dist/components/moni-select.d.ts +300 -35
  106. package/dist/components/moni-select.d.ts.map +1 -1
  107. package/dist/components/moni-select.js +323 -39
  108. package/dist/components/moni-side-sheet.d.ts +94 -32
  109. package/dist/components/moni-side-sheet.d.ts.map +1 -1
  110. package/dist/components/moni-side-sheet.js +94 -32
  111. package/dist/components/moni-slider.d.ts +152 -41
  112. package/dist/components/moni-slider.d.ts.map +1 -1
  113. package/dist/components/moni-slider.js +154 -42
  114. package/dist/components/moni-snackbar.d.ts +53 -43
  115. package/dist/components/moni-snackbar.d.ts.map +1 -1
  116. package/dist/components/moni-snackbar.js +53 -43
  117. package/dist/components/moni-split-button.d.ts +63 -18
  118. package/dist/components/moni-split-button.d.ts.map +1 -1
  119. package/dist/components/moni-split-button.js +63 -18
  120. package/dist/components/moni-step.d.ts +36 -24
  121. package/dist/components/moni-step.d.ts.map +1 -1
  122. package/dist/components/moni-step.js +41 -29
  123. package/dist/components/moni-stepper.d.ts +64 -27
  124. package/dist/components/moni-stepper.d.ts.map +1 -1
  125. package/dist/components/moni-stepper.js +72 -34
  126. package/dist/components/moni-switch.d.ts +57 -46
  127. package/dist/components/moni-switch.d.ts.map +1 -1
  128. package/dist/components/moni-switch.js +59 -47
  129. package/dist/components/moni-tab.d.ts +26 -18
  130. package/dist/components/moni-tab.d.ts.map +1 -1
  131. package/dist/components/moni-tab.js +29 -21
  132. package/dist/components/moni-tabs.d.ts +34 -28
  133. package/dist/components/moni-tabs.d.ts.map +1 -1
  134. package/dist/components/moni-tabs.js +34 -28
  135. package/dist/components/moni-text-field.d.ts +139 -29
  136. package/dist/components/moni-text-field.d.ts.map +1 -1
  137. package/dist/components/moni-text-field.js +157 -31
  138. package/dist/components/moni-textarea.d.ts +127 -29
  139. package/dist/components/moni-textarea.d.ts.map +1 -1
  140. package/dist/components/moni-textarea.js +131 -32
  141. package/dist/components/moni-time-picker.d.ts +114 -24
  142. package/dist/components/moni-time-picker.d.ts.map +1 -1
  143. package/dist/components/moni-time-picker.js +127 -33
  144. package/dist/components/moni-toolbar.d.ts +37 -23
  145. package/dist/components/moni-toolbar.d.ts.map +1 -1
  146. package/dist/components/moni-toolbar.js +37 -23
  147. package/dist/components/moni-tooltip.d.ts +100 -36
  148. package/dist/components/moni-tooltip.d.ts.map +1 -1
  149. package/dist/components/moni-tooltip.js +104 -40
  150. package/dist/components/moni-typography.d.ts +68 -30
  151. package/dist/components/moni-typography.d.ts.map +1 -1
  152. package/dist/components/moni-typography.js +70 -32
  153. package/dist/styles/tokens.css +7 -7
  154. package/dist/types/events.d.ts +17 -0
  155. package/dist/types/events.d.ts.map +1 -0
  156. package/dist/types/events.js +1 -0
  157. package/dist/utils/event-emitter.d.ts +13 -0
  158. package/dist/utils/event-emitter.d.ts.map +1 -0
  159. package/dist/utils/event-emitter.js +13 -0
  160. package/package.json +2 -1
  161. package/src/components/_base/moni-element.ts +73 -9
  162. package/src/components/index.ts +2 -2
  163. package/src/components/loading-shapes.ts +19 -0
  164. package/src/components/moni-app-bar.test.ts +10 -10
  165. package/src/components/moni-app-bar.ts +72 -62
  166. package/src/components/moni-badge.ts +64 -66
  167. package/src/components/moni-bottom-sheet.test.ts +39 -45
  168. package/src/components/moni-bottom-sheet.ts +222 -131
  169. package/src/components/moni-button-group.test.ts +14 -14
  170. package/src/components/moni-button-group.ts +107 -29
  171. package/src/components/moni-button-segment.ts +53 -12
  172. package/src/components/moni-button.test.ts +28 -26
  173. package/src/components/moni-button.ts +139 -39
  174. package/src/components/moni-card.test.ts +6 -6
  175. package/src/components/moni-card.ts +70 -50
  176. package/src/components/moni-carousel.test.ts +12 -12
  177. package/src/components/moni-carousel.ts +1129 -269
  178. package/src/components/moni-checkbox.test.ts +7 -7
  179. package/src/components/moni-checkbox.ts +62 -48
  180. package/src/components/moni-chip.test.ts +17 -17
  181. package/src/components/moni-chip.ts +112 -34
  182. package/src/components/moni-color-field.test.ts +5 -5
  183. package/src/components/moni-color-field.ts +108 -24
  184. package/src/components/moni-context-menu.test.ts +11 -11
  185. package/src/components/moni-context-menu.ts +76 -30
  186. package/src/components/moni-dialog.ts +108 -59
  187. package/src/components/moni-divider.test.ts +4 -4
  188. package/src/components/moni-divider.ts +42 -30
  189. package/src/components/moni-expansion.ts +32 -22
  190. package/src/components/moni-fab-menu.test.ts +11 -11
  191. package/src/components/moni-fab-menu.ts +69 -31
  192. package/src/components/moni-fab.test.ts +16 -16
  193. package/src/components/moni-fab.ts +100 -34
  194. package/src/components/moni-file-field.test.ts +8 -8
  195. package/src/components/moni-file-field.ts +132 -30
  196. package/src/components/moni-icon.test.ts +11 -11
  197. package/src/components/moni-icon.ts +43 -38
  198. package/src/components/moni-list-item.test.ts +10 -10
  199. package/src/components/moni-list-item.ts +61 -41
  200. package/src/components/moni-list.ts +37 -21
  201. package/src/components/moni-loading-indicator.test.ts +3 -3
  202. package/src/components/moni-loading-indicator.ts +54 -22
  203. package/src/components/moni-menu-item.ts +23 -16
  204. package/src/components/moni-menu.test.ts +7 -7
  205. package/src/components/moni-menu.ts +87 -30
  206. package/src/components/moni-morph-modal.test.ts +18 -17
  207. package/src/components/moni-morph-modal.ts +327 -60
  208. package/src/components/moni-nav-item.ts +59 -29
  209. package/src/components/moni-nav.test.ts +11 -11
  210. package/src/components/moni-nav.ts +94 -35
  211. package/src/components/moni-progress.test.ts +9 -9
  212. package/src/components/moni-progress.ts +100 -56
  213. package/src/components/moni-radio.test.ts +8 -8
  214. package/src/components/moni-radio.ts +71 -51
  215. package/src/components/moni-ripple.ts +74 -59
  216. package/src/components/moni-segmented-button.test.ts +4 -4
  217. package/src/components/moni-segmented-button.ts +94 -17
  218. package/src/components/moni-select-option.ts +30 -23
  219. package/src/components/moni-select.test.ts +11 -11
  220. package/src/components/moni-select.ts +361 -50
  221. package/src/components/moni-side-sheet.test.ts +7 -7
  222. package/src/components/moni-side-sheet.ts +94 -32
  223. package/src/components/moni-slider.test.ts +7 -7
  224. package/src/components/moni-slider.ts +165 -42
  225. package/src/components/moni-snackbar.test.ts +8 -8
  226. package/src/components/moni-snackbar.ts +53 -43
  227. package/src/components/moni-split-button.ts +63 -18
  228. package/src/components/moni-step.test.ts +7 -7
  229. package/src/components/moni-step.ts +41 -29
  230. package/src/components/moni-stepper.test.ts +7 -7
  231. package/src/components/moni-stepper.ts +72 -34
  232. package/src/components/moni-switch.test.ts +12 -12
  233. package/src/components/moni-switch.ts +66 -47
  234. package/src/components/moni-tab.test.ts +5 -5
  235. package/src/components/moni-tab.ts +29 -21
  236. package/src/components/moni-tabs.ts +34 -28
  237. package/src/components/moni-text-field.test.ts +13 -13
  238. package/src/components/moni-text-field.ts +181 -30
  239. package/src/components/moni-textarea.test.ts +17 -17
  240. package/src/components/moni-textarea.ts +155 -31
  241. package/src/components/moni-time-picker.test.ts +5 -5
  242. package/src/components/moni-time-picker.ts +128 -33
  243. package/src/components/moni-toolbar.test.ts +8 -8
  244. package/src/components/moni-toolbar.ts +37 -23
  245. package/src/components/moni-tooltip.test.ts +15 -15
  246. package/src/components/moni-tooltip.ts +107 -40
  247. package/src/components/moni-typography.test.ts +10 -10
  248. package/src/components/moni-typography.ts +73 -32
  249. package/src/styles/tokens.css +7 -7
  250. package/src/types/events.ts +17 -0
  251. package/src/utils/event-emitter.ts +26 -0
@@ -13,118 +13,290 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
13
13
  import { html, css } from 'lit';
14
14
  import { customElement, property, query, state, queryAll } from 'lit/decorators.js';
15
15
  import { MoniElement, sharedStyles } from './_base/index.js';
16
+ import { emitMoniEvent } from '../utils/event-emitter.js';
16
17
  import { gsap } from 'gsap';
17
18
  /**
18
- * Material Design 3 Expressive Carousel component.
19
+ * Componente Material Design 3 Expressive Carousel.
19
20
  *
20
- * Carousels display a collection of related items in a scrollable, horizontal list.
21
- * They allow users to quickly browse through items like images, cards, or products.
21
+ * Los carruseles muestran una colección de elementos relacionados en una lista horizontal desplazable.
22
+ * Permiten a los usuarios navegar rápidamente a través de elementos como imágenes, tarjetas, o productos.
22
23
  *
23
- * **M3 spec reference:** `m3-docs/components/carousel/specs.md`
24
+ * **Referencia de la especificación M3:** `m3-docs/components/carousel/specs.md`
24
25
  *
25
- * **Layout variants:**
26
- * - `multi-browse` (default) — Shows a mix of large, medium, and small (peeking)
27
- * items. Best for exploring a large number of items.
28
- * - `hero` — Focuses on one large primary item while showing a sliver of the
29
- * next item. Best for featuring important content.
30
- * - `uncontained` — Standard layout where all items have the same width and
31
- * bleed off the edges of the container.
26
+ * **Variantes de diseño:**
27
+ * - `multi-browse` (por defecto) — Muestra una mezcla de elementos grandes, medianos y pequeños (asomando).
28
+ * Mejor para explorar un gran número de elementos.
29
+ * - `hero` — Se enfoca en un elemento primario grande mientras muestra una fracción del
30
+ * siguiente elemento. Mejor para destacar contenido importante.
31
+ * - `uncontained` — Diseño estándar donde todos los elementos tienen el mismo ancho y
32
+ * se desbordan por los bordes del contenedor.
32
33
  *
33
- * **Animation & Gestures:**
34
- * This component uses GSAP for smooth drag, flick, and snap animations,
35
- * mirroring the high-fidelity M3 Expressive motion specs. It handles touch
36
- * gestures for mobile and mouse-drag for desktop.
34
+ * **Animación y Gestos:**
35
+ * Este componente usa GSAP para animaciones suaves de arrastre, deslizamiento y ajuste,
36
+ * reflejando las especificaciones de movimiento de alta fidelidad de M3 Expressive. Maneja
37
+ * gestos táctiles para móviles y arrastre del ratón para escritorio.
37
38
  *
38
- * **Auto-sizing (`auto` mode):**
39
- * When `auto=true` (default), the carousel measures its own width and
40
- * dynamically calculates the optimal sizes for large, medium, and small items
41
- * based on the active `layout` to ensure they fit perfectly without awkward gaps
42
- * or clipping at the edges.
39
+ * **Auto-tamaño (modo `auto`):**
40
+ * Cuando `auto=true` (por defecto), el carrusel mide su propio ancho y
41
+ * calcula dinámicamente los tamaños óptimos para los elementos grandes, medianos y pequeños
42
+ * basados en el `layout` activo para asegurar que encajen perfectamente sin huecos incómodos
43
+ * ni recortes en los bordes.
43
44
  *
44
45
  * @example
45
46
  * ```html
46
- * <!-- Declarative usage via DOM properties (recommended) -->
47
+ * <!-- Uso declarativo a través de propiedades DOM (recomendado) -->
47
48
  * <moni-carousel layout="hero"></moni-carousel>
48
49
  * <script>
49
50
  * const carousel = document.querySelector('moni-carousel');
50
51
  * carousel.items = [
51
- * { title: 'Item 1', img: '/img1.jpg', href: '/link1' },
52
- * { title: 'Item 2', img: '/img2.jpg' }
52
+ * { title: 'Elemento 1', img: '/img1.jpg', href: '/link1' },
53
+ * { title: 'Elemento 2', img: '/img2.jpg' }
53
54
  * ];
54
55
  * </script>
55
56
  *
56
- * <!-- Slot-based usage (for SSR or simple static content) -->
57
+ * <!-- Uso basado en slots (para SSR o contenido estático simple) -->
57
58
  * <moni-carousel layout="uncontained">
58
59
  * <div slot="item">
59
60
  * <img src="/img1.jpg" />
60
- * <h3>Static Item</h3>
61
+ * <h3>Elemento Estático</h3>
61
62
  * </div>
62
63
  * </moni-carousel>
63
64
  * ```
64
65
  *
65
- * @slot item - Alternative to the `items` property. Slot individual HTML elements
66
- * instead of passing data objects.
66
+ * @slot item - Alternativa a la propiedad `items`. Inserta elementos HTML individuales
67
+ * en lugar de pasar objetos de datos.
67
68
  *
68
- * @csspart carousel - The outer wrapper.
69
- * @csspart track - The scrolling track element.
70
- * @csspart item - Individual carousel item containers.
71
- * @csspart img - The image elements inside the items.
72
- * @csspart title - The title text elements inside the items.
69
+ * @csspart carousel - El envoltorio exterior.
70
+ * @csspart track - El elemento de la pista desplazable.
71
+ * @csspart item - Contenedores de elementos del carrusel individuales.
72
+ * @csspart img - Los elementos de imagen dentro de los elementos.
73
+ * @csspart title - Los elementos de texto de título dentro de los elementos.
73
74
  */
74
75
  let MoniCarousel = class MoniCarousel extends MoniElement {
75
76
  constructor() {
76
77
  super(...arguments);
78
+ /**
79
+ * Array de elementos a mostrar en el carrusel.
80
+ * Cada elemento requiere al menos `title` e `img`. El opcional `href` renderiza la tarjeta como un enlace.
81
+ * @default []
82
+ */
77
83
  this.items = [];
84
+ /**
85
+ * Variante de diseño visual. Determina cuántos elementos son visibles y cómo escalan.
86
+ * - `'multi-browse'` (por defecto) — Elementos grandes + medianos + pequeños (asomando).
87
+ * - `'hero'` — Uno o más elementos grandes + elemento pequeño asomando.
88
+ * - `'uncontained'` — Elementos de igual ancho que se desbordan por el borde.
89
+ * @default 'multi-browse'
90
+ */
78
91
  this.layout = 'multi-browse';
92
+ /**
93
+ * Cuando es `true`, el carrusel calcula los tamaños óptimos de los elementos automáticamente
94
+ * basado en el ancho del contenedor y la variante de `layout` activa.
95
+ * Cuando es `false`, usa los valores explícitos `largeWidth`, `mediumWidth`, `smallWidth`.
96
+ * @default true
97
+ */
79
98
  this.auto = true;
99
+ /**
100
+ * Ancho en píxeles para el elemento grande (enfoque principal) del carrusel.
101
+ * Solo se usa cuando `auto=false`.
102
+ * @default 220
103
+ */
80
104
  this.largeWidth = 220;
105
+ /**
106
+ * Ancho en píxeles para el elemento de tamaño mediano (secundario) del carrusel.
107
+ * Solo se usa cuando `auto=false` y `layout='multi-browse'`.
108
+ * @default 96
109
+ */
81
110
  this.mediumWidth = 96;
111
+ /**
112
+ * Ancho en píxeles para el elemento pequeño (asomando) del carrusel.
113
+ * Solo se usa cuando `auto=false`.
114
+ * @default 48
115
+ */
82
116
  this.smallWidth = 48;
117
+ /**
118
+ * Espacio en píxeles entre los elementos del carrusel.
119
+ * @default 8
120
+ */
83
121
  this.gap = 8;
122
+ /**
123
+ * Relleno horizontal en píxeles aplicado al inicio de la pista del carrusel.
124
+ * @default 16
125
+ */
84
126
  this.padding = 16;
127
+ /**
128
+ * Radio del borde en píxeles aplicado a cada tarjeta.
129
+ * @default 28
130
+ */
85
131
  this.borderRadius = 28;
132
+ /**
133
+ * Cuando es `true`, renderiza un enlace "Mostrar todo" en el encabezado.
134
+ * @default false
135
+ */
86
136
  this.showAll = false;
137
+ /**
138
+ * Texto de etiqueta para el enlace "Mostrar todo".
139
+ * @default 'Show all'
140
+ */
87
141
  this.showAllText = 'Show all';
142
+ /**
143
+ * Encabezado de sección opcional renderizado sobre la pista del carrusel.
144
+ * @default ''
145
+ */
88
146
  this.headerText = '';
147
+ /**
148
+ * Cuando es `true`, oculta los botones de flecha de navegación anterior/siguiente.
149
+ * @default false
150
+ */
89
151
  this.hideNav = false;
152
+ /**
153
+ * Cuando es `true`, habilita el bucle infinito sin interrupciones clonando la lista de elementos
154
+ * a través de un búfer de desplazamiento virtual grande, luego reposicionando silenciosamente
155
+ * el desplazamiento cuando el usuario se acerca a cualquier borde.
156
+ * @default false
157
+ */
158
+ this.infinite = false;
159
+ /**
160
+ * Cuando es `true`, avanza automáticamente el carrusel en el intervalo definido
161
+ * por `autoplayInterval`. La reproducción automática se detiene durante las interacciones de arrastre activas.
162
+ * @default false
163
+ */
164
+ this.autoplay = false;
165
+ /**
166
+ * Milisegundos entre los avances automáticos de diapositivas cuando `autoplay=true`.
167
+ * @default 3000
168
+ */
169
+ this.autoplayInterval = 3000;
170
+ /**
171
+ * Ancho medido del elemento host del carrusel en píxeles.
172
+ * Actualizado por el `ResizeObserver` en cada redimensionamiento del contenedor y usado por
173
+ * `computedLayout` para calcular cuántos elementos caben en la pantalla.
174
+ */
90
175
  this._containerWidth = 0;
176
+ /**
177
+ * Elementos analizados desde el slot Light DOM (uso declarativo).
178
+ * Poblado por `_handleSlotChange` cuando los usuarios insertan elementos `<img>` o `<a><img></a>`.
179
+ */
91
180
  this._slottedItems = [];
181
+ /**
182
+ * Si el slot `show-all` contiene contenido proyectado por el usuario.
183
+ * Controla si se renderiza el área "Mostrar todo" del encabezado.
184
+ */
92
185
  this._hasSlottedShowAll = false;
186
+ /** Instancia ResizeObserver que observa las dimensiones del elemento host. */
93
187
  this._resizeObserver = null;
94
- this._isDown = false;
95
- this._startX = 0;
96
- this._scrollLeftStart = 0;
97
- this._velocity = 0;
98
- this._lastTime = 0;
99
- this._lastX = 0;
100
- this._draggedDistance = 0;
101
- this._rafId = null;
188
+ /** Estado interno de visibilidad (por índice lógico) para evitar falsos eventos al ciclar el scroll infinito */
189
+ this._logicalItemInViewport = [];
190
+ /** `true` mientras el usuario arrastra activamente (ratón o toque presionado). */
191
+ this._isDragging = false;
192
+ /** Coordenada X en el inicio del gesto de arrastre actual (relativa al contenedor de desplazamiento). */
193
+ this._dragStartX = 0;
194
+ /** Valor `scrollLeft` del contenedor en el momento en que comenzó el arrastre. */
195
+ this._scrollLeftAtDragStart = 0;
196
+ /**
197
+ * Velocidad del puntero en píxeles por milisegundo.
198
+ * Calculada gradualmente durante el arrastre y usada para el impulso/inercia al soltar.
199
+ */
200
+ this._pointerVelocity = 0;
201
+ /** Marca de tiempo (de `performance.now()`) del último evento de movimiento del puntero/toque. */
202
+ this._lastPointerTime = 0;
203
+ /** Coordenada X del puntero en el cuadro del evento de movimiento anterior. */
204
+ this._lastPointerX = 0;
205
+ /**
206
+ * Distancia horizontal absoluta acumulada (en píxeles) recorrida durante el arrastre actual.
207
+ * Usada para distinguir un deslizamiento intencional de un clic accidental (umbral: 8px).
208
+ */
209
+ this._totalDraggedDistance = 0;
210
+ /**
211
+ * ID de `requestAnimationFrame` activo del bucle de desplazamiento por inercia.
212
+ * Almacenado para que pueda cancelarse cuando el usuario inicia un nuevo arrastre.
213
+ */
214
+ this._animationFrameId = null;
215
+ /** `true` cuando el ancho de la ventana gráfica coincide con el punto de ruptura móvil (`max-width: 600px`). */
102
216
  this._isMobile = false;
103
- // Scroll interpolation state
217
+ // ─── Estado de interpolación GSAP Ticker ──────────────────────────────────────
218
+ /**
219
+ * La posición de desplazamiento hacia la que el ticker de GSAP está interpolando (lerping) **en cada cuadro**.
220
+ * Expresada en unidades de `itemSize` (no píxeles) para matemáticas independientes del diseño.
221
+ */
104
222
  this._tickerTarget = 0;
223
+ /**
224
+ * La posición de desplazamiento interpolada actual usada para impulsar el diseño visual.
225
+ * Interpola hacia `_tickerTarget` en cada cuadro, creando el efecto de suavizado.
226
+ */
105
227
  this._tickerCurrent = 0;
228
+ /**
229
+ * `true` mientras el callback del ticker de GSAP (`_tick`) está registrado y en ejecución.
230
+ * Evita añadir el callback del ticker múltiples veces en llamadas concurrentes.
231
+ */
106
232
  this._isTicking = false;
107
- // quickSetter caches avoid gsap.set() overhead per frame
233
+ // ─── Estado de Bucle Infinito & Reproducción Automática ─────────────────────────────────────
234
+ /**
235
+ * Protege la inicialización de una sola vez de la posición del búfer de desplazamiento infinito.
236
+ * Se establece en `true` una vez que el contenedor de desplazamiento se ha reposicionado
237
+ * silenciosamente al centro del búfer virtual después del primer renderizado.
238
+ */
239
+ this._infiniteInitialized = false;
240
+ /** ID del temporizador devuelto por `setInterval()` para la función de reproducción automática. */
241
+ this._autoplayTimer = null;
242
+ // ─── Cachés de GSAP quickSetter ───────────────────────────────────────────────
243
+ // Las funciones quickSetter evitan la sobrecarga de llamar a `gsap.set()` en cada cuadro.
244
+ // Pre-construidas una vez después de cada ciclo de renderizado; invalidadas cuando cambia el número de tarjetas.
245
+ /** Array de quickSetters de GSAP para el CSS `transform: translateX()` de cada tarjeta. */
108
246
  this._cardSetX = [];
247
+ /** Array de quickSetters de GSAP para el `width` en línea de cada tarjeta. */
109
248
  this._cardSetWidth = [];
249
+ /** Array de quickSetters de GSAP para el parallax `translateX` del `<img>` de cada tarjeta. */
110
250
  this._imgSetX = [];
251
+ /** Array de quickSetters de GSAP para la `opacity` del elemento `.card-title` de cada tarjeta. */
111
252
  this._titleSetOpacity = [];
112
253
  /**
113
- * RAF-gated version of updateLayout for scroll events.
114
- * Prevents multiple layout recalculations per frame.
254
+ * Callback del Ticker de GSAP el bucle de animación principal para el carrusel.
255
+ *
256
+ * Esta función está registrada con `gsap.ticker.add()` y se ejecuta en cada
257
+ * cuadro de animación (sincronizado con `requestAnimationFrame`). Implementa
258
+ * **interpolación exponencial (smoothstep)** para interpolar `_tickerCurrent` hacia
259
+ * `_tickerTarget`, creando una desaceleración de suavizado físicamente plausible.
260
+ *
261
+ * La fórmula de interpolación: `current += (target - current) × easeStrength × deltaTimeRatio`
262
+ *
263
+ * - `easeStrength = 0.08` en móvil: acoplamiento más estrecho para evitar que el carrusel
264
+ * se sienta "lento" o desincronizado con un dedo.
265
+ * - `easeStrength = 0.15` en escritorio: ligeramente más suelto para una sensación más cinematográfica.
266
+ * - `deltaTimeRatio` normaliza la velocidad de animación a través de diferentes velocidades de cuadro
267
+ * (60fps vs 120fps), manteniendo la atenuación percibida constante.
268
+ *
269
+ * Una vez que la diferencia entre el objetivo y el actual cae por debajo de 0.0005 (medio
270
+ * sub-píxel), la animación se considera completa y el ticker se elimina
271
+ * para conservar los recursos de CPU/GPU entre interacciones.
115
272
  */
116
273
  this._tick = () => {
117
- const dt = gsap.ticker.deltaRatio();
118
- // Smoother lerp on mobile to avoid abrupt changes ("brusco")
119
- const ease = this._isMobile ? 0.08 : 0.15;
120
- this._tickerCurrent += (this._tickerTarget - this._tickerCurrent) * ease * dt;
274
+ // `deltaTimeRatio` normalizes frame-rate differences. At 60fps it is ≈1;
275
+ // at 120fps it is ≈0.5, preventing double-speed animations on high-refresh screens.
276
+ const deltaTimeRatio = gsap.ticker.deltaRatio();
277
+ // Tighter lerp factor on mobile for finger-tracking accuracy;
278
+ // looser on desktop for cinematic smoothness.
279
+ const easeStrength = this._isMobile ? 0.08 : 0.15;
280
+ // Lerp exponencial: mueve una fracción de la distancia restante en cada cuadro.
281
+ this._tickerCurrent += (this._tickerTarget - this._tickerCurrent) * easeStrength * deltaTimeRatio;
282
+ // Umbral de convergencia: detener la animación cuando sea visualmente indistinguible del objetivo.
283
+ // 0.0005 is well below sub-pixel precision, so no visible jitter will occur.
121
284
  if (Math.abs(this._tickerTarget - this._tickerCurrent) < 0.0005) {
122
285
  this._tickerCurrent = this._tickerTarget;
123
286
  gsap.ticker.remove(this._tick);
124
287
  this._isTicking = false;
288
+ // Después de que la animación se asiente, comprobar si necesitamos reposicionar
289
+ // silenciosamente el búfer de desplazamiento para un bucle infinito sin interrupciones.
290
+ this._checkInfiniteWrap();
125
291
  }
292
+ // Renderizar todas las tarjetas en la nueva posición de desplazamiento interpolada.
126
293
  this._applyLayout(this._tickerCurrent);
127
294
  };
295
+ /**
296
+ * Lee la posición real del scroll nativo y arranca el ticker de animación si no está corriendo.
297
+ * Esta separación es crucial para el rendimiento: leemos el DOM aquí (scrollLeft) pero
298
+ * escribimos los estilos en `_tick` -> `_applyLayout`.
299
+ */
128
300
  this._scheduleLayout = () => {
129
301
  if (!this._scrollContainer)
130
302
  return;
@@ -134,14 +306,43 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
134
306
  gsap.ticker.add(this._tick);
135
307
  }
136
308
  };
309
+ /**
310
+ * Función principal de sincronización de estado.
311
+ * Refresca la lógica del scroll infinito y fuerza una recolección de los setters de GSAP
312
+ * si la cantidad de tarjetas cambió. Puede saltarse el ticker y ser instantánea (ej. al redimensionar la ventana).
313
+ */
137
314
  this.updateLayout = (instant = false) => {
138
315
  if (!this._scrollContainer || !this._cards || this._cards.length === 0)
139
316
  return;
140
- // Rebuild quickSetters if card count changed (e.g. after items update)
317
+ // Re-cacheamos las referencias DOM de GSAP si la cantidad de tarjetas varió (e.g., inserción dinámica de nodos)
141
318
  if (this._cardSetX.length !== this._cards.length) {
142
319
  this._buildQuickSetters();
143
320
  }
321
+ // Lógica de montaje inicial para el carrusel infinito
322
+ if (this.infinite && !this._infiniteInitialized && this.effectiveItems.length > 0) {
323
+ const C = this.effectiveItems.length;
324
+ const V = C * this._visualCardsMultiplier;
325
+ const K = this._snapSetsCount;
326
+ const centerSetIndex = Math.floor(K / 2);
327
+ // Doble rAF asegura que el navegador completó el reflow de CSS (scrollWidth) antes de inyectar el scroll
328
+ requestAnimationFrame(() => {
329
+ setTimeout(() => {
330
+ if (this._scrollContainer) {
331
+ // Forzamos el recálculo interno del layout
332
+ this._scrollContainer.scrollWidth;
333
+ // Desplazamos silenciosamente al usuario hacia la mitad del contenedor clonado
334
+ this._scrollContainer.scrollLeft = centerSetIndex * V * this.itemSize;
335
+ // Forzamos al ticker visual a asimilar la posición de golpe, evitando un "flash" de animación desde 0
336
+ this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
337
+ this._tickerCurrent = this._tickerTarget;
338
+ this._applyLayout(this._tickerCurrent);
339
+ }
340
+ }, 20);
341
+ });
342
+ this._infiniteInitialized = true;
343
+ }
144
344
  this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
345
+ // Si pasamos instant=true, bypassamos el smoothing temporalmente
145
346
  if (instant || (!this._isTicking && this._tickerCurrent === 0)) {
146
347
  this._tickerCurrent = this._tickerTarget;
147
348
  this._applyLayout(this._tickerCurrent);
@@ -154,6 +355,11 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
154
355
  }
155
356
  };
156
357
  }
358
+ /**
359
+ * Devuelve la fuente del elemento activo.
360
+ * La propiedad programática `items` tiene prioridad sobre los hijos Light DOM en slots.
361
+ * Recurre a `_slottedItems` cuando no se proporcionan elementos programáticos.
362
+ */
157
363
  get effectiveItems() {
158
364
  return this.items && this.items.length > 0 ? this.items : this._slottedItems;
159
365
  }
@@ -216,6 +422,8 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
216
422
  overflow-x: auto;
217
423
  overflow-y: hidden;
218
424
  scroll-snap-type: x mandatory;
425
+ scroll-padding-left: var(--carousel-padding, 16px);
426
+ scroll-padding-right: var(--carousel-padding, 16px);
219
427
  scrollbar-width: none;
220
428
  -webkit-overflow-scrolling: touch;
221
429
  cursor: grab;
@@ -383,6 +591,12 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
383
591
  }
384
592
  `
385
593
  ]; }
594
+ /**
595
+ * Hook del ciclo de vida (Lit).
596
+ * Inicializa el `ResizeObserver` para reaccionar a cambios en el tamaño del contenedor,
597
+ * lo que es crucial para recalcular el `computedLayout` (cuántas cards caben en el viewport).
598
+ * También añade listeners globales pasivos para eventos de redimensionamiento de ventana.
599
+ */
386
600
  connectedCallback() {
387
601
  super.connectedCallback();
388
602
  if (typeof ResizeObserver !== 'undefined') {
@@ -405,14 +619,19 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
405
619
  this.__mqlCleanup = () => mql.removeEventListener('change', handler);
406
620
  }
407
621
  }
622
+ /**
623
+ * Hook del ciclo de vida (Lit).
624
+ * Limpia y desconecta el `ResizeObserver`, el `IntersectionObserver` y elimina
625
+ * los event listeners globales para evitar fugas de memoria (memory leaks).
626
+ */
408
627
  disconnectedCallback() {
409
628
  if (this._resizeObserver) {
410
629
  this._resizeObserver.disconnect();
411
630
  }
412
631
  // Cancel any pending RAF or ticker
413
- if (this._rafId !== null) {
414
- cancelAnimationFrame(this._rafId);
415
- this._rafId = null;
632
+ if (this._animationFrameId !== null) {
633
+ cancelAnimationFrame(this._animationFrameId);
634
+ this._animationFrameId = null;
416
635
  }
417
636
  gsap.ticker.remove(this._tick);
418
637
  this._isTicking = false;
@@ -423,6 +642,11 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
423
642
  }
424
643
  super.disconnectedCallback();
425
644
  }
645
+ /**
646
+ * Hook del ciclo de vida (Lit) ejecutado tras el primer render.
647
+ * Inspecciona el Shadow DOM para ubicar el slot por defecto y procesar
648
+ * las cards iniciales (`_handleSlotChange` invocado manualmente).
649
+ */
426
650
  firstUpdated() {
427
651
  const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])');
428
652
  if (defaultSlot) {
@@ -434,7 +658,19 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
434
658
  }
435
659
  this._buildQuickSetters();
436
660
  this.updateLayout(true);
661
+ if (this.autoplay) {
662
+ this._startAutoplay();
663
+ }
437
664
  }
665
+ /**
666
+ * Hook del ciclo de vida (Lit). Se invoca tras cada actualización reactiva.
667
+ *
668
+ * Lógica principal:
669
+ * - Si cambian propiedades estructurales (`autoplay`, `infinite`, `gap`), reconstruye el
670
+ * layout y detiene/inicia el timer de autoplay.
671
+ * - Delega en `_checkInfiniteWrap()` para reposicionar el scroll si estamos cerca
672
+ * de los bordes del carrusel infinito.
673
+ */
438
674
  updated(changedProperties) {
439
675
  super.updated(changedProperties);
440
676
  if (changedProperties.has('layout') ||
@@ -445,179 +681,307 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
445
681
  changedProperties.has('padding') ||
446
682
  changedProperties.has('items') ||
447
683
  changedProperties.has('hideNav') ||
684
+ changedProperties.has('infinite') ||
685
+ changedProperties.has('autoplay') ||
686
+ changedProperties.has('autoplayInterval') ||
448
687
  changedProperties.has('_containerWidth') ||
449
688
  changedProperties.has('_slottedItems') ||
450
689
  changedProperties.has('_hasSlottedShowAll')) {
690
+ if (changedProperties.has('infinite') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
691
+ this._infiniteInitialized = false;
692
+ }
451
693
  if (changedProperties.has('layout') && this._scrollContainer) {
452
694
  this._scrollContainer.scrollLeft = 0;
695
+ this._infiniteInitialized = false;
453
696
  this._scrollContainer.style.scrollSnapType =
454
697
  this.layout === 'uncontained' ? 'none' : 'x mandatory';
455
698
  }
456
699
  this.updateLayout(true);
700
+ if (changedProperties.has('autoplay') || changedProperties.has('autoplayInterval') || changedProperties.has('items') || changedProperties.has('_slottedItems')) {
701
+ if (this.autoplay) {
702
+ this._startAutoplay();
703
+ }
704
+ else {
705
+ this._stopAutoplay();
706
+ }
707
+ }
457
708
  }
458
709
  }
710
+ /**
711
+ * Motor de cálculo del Layout.
712
+ *
713
+ * Basado en las proporciones de Material 3 para carruseles:
714
+ * Calcula el ancho disponible (`W_c`) y determina el número de ítems grandes (`L`),
715
+ * medianos (`M`) y pequeños (`S`) que caben en pantalla, usando la fórmula de la
716
+ * especificación (multi-browse, hero o uncontained).
717
+ *
718
+ * Retorna `{ L, M, S, N }` donde `N` es el divisor base para calcular tamaños proporcionales.
719
+ */
459
720
  get computedLayout() {
460
- const W_c = this._containerWidth || this.getBoundingClientRect().width || 360;
721
+ /**
722
+ * `containerWidth` — ancho total en píxeles del elemento host del carrusel.
723
+ * Recurre a `getBoundingClientRect()` si el ResizeObserver aún no se ha disparado,
724
+ * y en última instancia por defecto a 360px (un ancho común de ventana gráfica móvil).
725
+ */
726
+ const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
461
727
  const gap = this.gap;
462
728
  const padding = this.padding;
463
- const W_a = W_c - 2 * padding;
464
- let L = this.largeWidth;
465
- let M = this.mediumWidth;
466
- let S = this.smallWidth;
467
- let N = 1;
729
+ /**
730
+ * `availableWidth` el ancho de pista utilizable después de restar el padding
731
+ * inicial y final del ancho total del contenedor. Este es el espacio que el algoritmo de diseño
732
+ * divide en ranuras de elementos grandes, medianos y pequeños.
733
+ * Fórmula: availableWidth = containerWidth - 2 × padding
734
+ */
735
+ const availableWidth = containerWidth - 2 * padding;
736
+ /** `largeItemWidth` — ancho calculado o configurado (px) de los elementos de enfoque principal. */
737
+ let largeItemWidth = this.largeWidth;
738
+ /** `mediumItemWidth` — ancho calculado o configurado (px) de los elementos secundarios. */
739
+ let mediumItemWidth = this.mediumWidth;
740
+ /** `smallItemWidth` — ancho calculado o configurado (px) de los elementos pequeños/asomando. */
741
+ let smallItemWidth = this.smallWidth;
742
+ /**
743
+ * `largeItemCount` — el número de elementos grandes visibles simultáneamente en la ventana gráfica.
744
+ * La especificación M3 define la fórmula de la ventana gráfica como:
745
+ * availableWidth = largeItemCount × largeItemWidth + mediumItemWidth + smallItemWidth + (largeItemCount + 1) × gap
746
+ */
747
+ let largeItemCount = 1;
468
748
  if (this.auto) {
469
749
  if (this.layout === 'uncontained') {
470
- // Fits as many L items as possible, letting them bleed off the right edge.
471
- // We target large width around 260px.
472
- L = Math.min(W_a, 260);
750
+ // En diseño 'uncontained', todos los elementos tienen el mismo ancho y se desbordan por el borde.
751
+ // We target a comfortable width around 260px, but cap it to availableWidth.
752
+ largeItemWidth = Math.min(availableWidth, 260);
473
753
  }
474
754
  else if (this.layout === 'hero') {
475
- // Hero has N large items and 1 small peeking item.
476
- // W_a = N * L + S + N * gap
477
- S = 48;
478
- N = Math.max(1, Math.floor((W_a - S) / (250 + gap)));
479
- L = (W_a - S - N * gap) / N;
480
- if (L < 150) {
481
- N = 1;
482
- L = W_a - S - gap;
755
+ // M3 Hero layout formula:
756
+ // availableWidth = largeItemCount × largeItemWidth + smallItemWidth + largeItemCount × gap
757
+ // We fix smallItemWidth = 48px (M3 spec minimum peeking width), then solve for largeItemWidth.
758
+ smallItemWidth = 48;
759
+ largeItemCount = Math.max(1, Math.floor((availableWidth - smallItemWidth) / (250 + gap)));
760
+ largeItemWidth = (availableWidth - smallItemWidth - largeItemCount * gap) / largeItemCount;
761
+ // Límite de seguridad: si el elemento grande fuera menor a 150px, forzar modo de un solo elemento.
762
+ if (largeItemWidth < 150) {
763
+ largeItemCount = 1;
764
+ largeItemWidth = availableWidth - smallItemWidth - gap;
483
765
  }
484
766
  }
485
767
  else {
486
- // multi-browse: N large items, 1 medium, 1 small
487
- // W_a = N * L + M + S + (N + 1) * gap
488
- S = 48;
489
- let bestN = 1;
490
- let bestL = 220;
768
+ // M3 Multi-Browse layout formula:
769
+ // availableWidth = largeItemCount × largeItemWidth + mediumItemWidth + smallItemWidth + (largeItemCount + 1) × gap
770
+ // mediumItemWidth = 0.45 × largeItemWidth (derived from M3 spec proportions)
771
+ // Solving: largeItemWidth = (availableWidth - smallItemWidth - (largeItemCount + 1) × gap) / (largeItemCount + 0.45)
772
+ smallItemWidth = 48;
773
+ // Optimización: probar valores de largeItemCount de 1 a 10 y elegir aquel cuyo
774
+ // largeItemWidth esté más cerca del objetivo ideal de 220px.
775
+ let bestLargeItemCount = 1;
776
+ let bestLargeItemWidth = 220;
491
777
  let bestDiff = Infinity;
492
- for (let n = 1; n <= 10; n++) {
493
- const l = (W_a - S - (n + 1) * gap) / (n + 0.45);
494
- if (l >= 140 && l <= 340) {
495
- const diff = Math.abs(l - 220);
778
+ for (let candidateCount = 1; candidateCount <= 10; candidateCount++) {
779
+ // Solve for largeItemWidth given this candidateCount:
780
+ const candidateLargeWidth = (availableWidth - smallItemWidth - (candidateCount + 1) * gap) / (candidateCount + 0.45);
781
+ // Only accept widths within a reasonable human-perceivable range [140px, 340px].
782
+ if (candidateLargeWidth >= 140 && candidateLargeWidth <= 340) {
783
+ const diff = Math.abs(candidateLargeWidth - 220);
496
784
  if (diff < bestDiff) {
497
785
  bestDiff = diff;
498
- bestN = n;
499
- bestL = l;
786
+ bestLargeItemCount = candidateCount;
787
+ bestLargeItemWidth = candidateLargeWidth;
500
788
  }
501
789
  }
502
790
  }
503
791
  if (bestDiff === Infinity) {
504
- N = 1;
505
- L = (W_a - S - 2 * gap) / 1.45;
792
+ // Fallback: no valid solution found; use single large item with minimum spacing.
793
+ largeItemCount = 1;
794
+ largeItemWidth = (availableWidth - smallItemWidth - 2 * gap) / 1.45;
506
795
  }
507
796
  else {
508
- N = bestN;
509
- L = bestL;
797
+ largeItemCount = bestLargeItemCount;
798
+ largeItemWidth = bestLargeItemWidth;
510
799
  }
511
- M = 0.45 * L;
800
+ // Especificación M3: el elemento mediano es el 45% del ancho del elemento grande.
801
+ mediumItemWidth = 0.45 * largeItemWidth;
512
802
  }
513
803
  }
514
804
  else {
515
- // Non-auto/manual sizing mode
516
- L = this.largeWidth;
517
- M = this.mediumWidth;
518
- S = this.smallWidth;
805
+ // ── Manual sizing mode (auto=false) ──────────────────────────────────
806
+ // Usar los valores explícitos de propiedad proporcionados por el consumidor.
807
+ largeItemWidth = this.largeWidth;
808
+ mediumItemWidth = this.mediumWidth;
809
+ smallItemWidth = this.smallWidth;
519
810
  if (this.layout === 'hero') {
520
- N = 1;
811
+ largeItemCount = 1;
521
812
  }
522
813
  else if (this.layout === 'uncontained') {
523
- N = Math.max(1, Math.floor(W_a / (L + gap)));
814
+ // En modo uncontained, calcular cuántos elementos grandes caben en el ancho disponible.
815
+ largeItemCount = Math.max(1, Math.floor(availableWidth / (largeItemWidth + gap)));
524
816
  }
525
817
  else {
526
- N = 1;
818
+ largeItemCount = 1;
527
819
  }
528
820
  }
529
- return { L, M, S, N };
821
+ // Retornar los cuatro valores calculados como propiedades nombradas.
822
+ // Callers use destructuring: const { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount } = this.computedLayout;
823
+ return { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount };
530
824
  }
825
+ /**
826
+ * Calcula el ancho total (incluyendo el gap) de un ítem de tamaño 'L' (Large).
827
+ * Este valor se utiliza como unidad de medida (step) para el Scroll Snapping
828
+ * y el cálculo de desplazamiento de páginas (sets).
829
+ */
531
830
  get itemSize() {
532
- return this.computedLayout.L + this.gap;
831
+ // El paso de ajuste es el ancho completo de la ranura: ancho del elemento más una unidad de espacio.
832
+ // Usado como la unidad de medida para el ajuste de desplazamiento, cálculos de impulso,
833
+ // y el posicionamiento del búfer de desplazamiento infinito.
834
+ return this.computedLayout.largeItemWidth + this.gap;
533
835
  }
534
- _getCardLayout(p) {
535
- const { L, M, S, N } = this.computedLayout;
836
+ /**
837
+ * Algoritmo Central del Carrusel M3.
838
+ *
839
+ * Calcula el ancho visual, la posición horizontal y la opacidad para una sola tarjeta del carrusel
840
+ * basándose en su posición normalizada en relación con la ventana gráfica actualmente visible.
841
+ *
842
+ * Este es el corazón del efecto "Masked Carousel" de M3: a medida que una tarjeta transita de
843
+ * fuera de pantalla a en pantalla (posición -1 → 0 → N), su ancho se interpola de
844
+ * `smallItemWidth` → `largeItemWidth`, y su opacidad aumenta.
845
+ *
846
+ * Semántica de posición:
847
+ * - `normalizedPosition < -1` → tarjeta está fuera de pantalla a la izquierda (oculta)
848
+ * - `normalizedPosition = 0` → tarjeta está en la ranura grande más a la izquierda (enfoque principal)
849
+ * - `normalizedPosition = N-1` → tarjeta está en la ranura grande más a la derecha
850
+ * - `normalizedPosition > N` → tarjeta está fuera de pantalla a la derecha (oculta)
851
+ *
852
+ * @param normalizedPosition - El índice de posición fraccional de la tarjeta en relación con el progreso
853
+ * de desplazamiento actual. Un valor de `0` significa que la tarjeta está perfectamente alineada con la ranura
854
+ * grande inicial; los valores fraccionales representan transiciones parciales.
855
+ * @returns Objeto con `{ width, x, opacity }` para aplicar directamente al elemento de la tarjeta.
856
+ */
857
+ _getCardLayout(normalizedPosition) {
858
+ const { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount } = this.computedLayout;
536
859
  const gap = this.gap;
537
860
  const padding = this.padding;
538
- let width = L;
539
- let x = padding;
861
+ // Default: full large-item width, primary slot position, fully visible.
862
+ let width = largeItemWidth;
863
+ let horizontalPosition = padding;
540
864
  let opacity = 1;
865
+ let visible = true;
541
866
  if (this.layout === 'uncontained') {
542
- width = L;
543
- x = padding + p * (L + gap);
867
+ // Uncontained: all items same width, positioned linearly without masking.
868
+ width = largeItemWidth;
869
+ horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
544
870
  opacity = 1;
871
+ const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
872
+ visible = (horizontalPosition + width > 0) && (horizontalPosition < containerWidth);
545
873
  }
546
874
  else if (this.layout === 'hero') {
547
- const X_N = padding + N * (L + gap);
548
- const X_M1 = padding - gap - S;
549
- if (p <= -1) {
550
- width = S;
551
- x = X_M1 + (p - -1) * (S + gap);
875
+ // ── Anclas de posición para el diseño 'hero' ──────────────────────────
876
+ // `xAfterLastLargeSlot` la posición X en píxeles inmediatamente después del último elemento grande.
877
+ // Aquí es donde comienza el elemento pequeño asomando.
878
+ const xAfterLastLargeSlot = padding + largeItemCount * (largeItemWidth + gap);
879
+ // `xBeforeFirstLargeSlot` la posición X en píxeles del elemento pequeño asomando a la izquierda.
880
+ // Alineado al borde negativo exacto para que repose completamente fuera del contenedor.
881
+ const xBeforeFirstLargeSlot = -smallItemWidth;
882
+ if (normalizedPosition <= -1) {
883
+ // La tarjeta está oculta fuera de la pantalla a la izquierda, apilada detrás del elemento asomando.
884
+ width = smallItemWidth;
885
+ horizontalPosition = xBeforeFirstLargeSlot + (normalizedPosition - -1) * (smallItemWidth + Math.max(gap, padding));
552
886
  opacity = 0;
887
+ visible = false;
553
888
  }
554
- else if (p <= 0) {
555
- width = gsap.utils.interpolate(S, L, p + 1);
556
- x = gsap.utils.interpolate(X_M1, padding, p + 1);
557
- opacity = gsap.utils.interpolate(0, 1, p + 1);
889
+ else if (normalizedPosition <= 0) {
890
+ // La tarjeta está en transición de entrada desde la izquierda: morphing de pequeño a grande.
891
+ // `normalizedPosition + 1` reasigna el rango [-1, 0] a [0, 1] para la interpolación.
892
+ width = gsap.utils.interpolate(smallItemWidth, largeItemWidth, normalizedPosition + 1);
893
+ horizontalPosition = gsap.utils.interpolate(xBeforeFirstLargeSlot, padding, normalizedPosition + 1);
894
+ opacity = gsap.utils.interpolate(0, 1, normalizedPosition + 1);
895
+ visible = true;
558
896
  }
559
- else if (p <= N - 1) {
560
- width = L;
561
- x = padding + p * (L + gap);
897
+ else if (normalizedPosition <= largeItemCount - 1) {
898
+ // La tarjeta está en una de las ranuras grandes (completamente visible).
899
+ width = largeItemWidth;
900
+ horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
562
901
  opacity = 1;
902
+ visible = true;
563
903
  }
564
- else if (p <= N) {
565
- const u = p - (N - 1);
566
- const X_last = padding + (N - 1) * (L + gap);
567
- width = gsap.utils.interpolate(L, S, u);
568
- x = gsap.utils.interpolate(X_last, X_N, u);
569
- opacity = gsap.utils.interpolate(1, 0, u);
904
+ else if (normalizedPosition <= largeItemCount) {
905
+ // La tarjeta está en transición de salida a la derecha: morphing de grande a pequeño (asomando).
906
+ // `transitionProgress` reasigna [N-1, N] a [0, 1].
907
+ const transitionProgress = normalizedPosition - (largeItemCount - 1);
908
+ const xLastLargeSlot = padding + (largeItemCount - 1) * (largeItemWidth + gap);
909
+ width = gsap.utils.interpolate(largeItemWidth, smallItemWidth, transitionProgress);
910
+ horizontalPosition = gsap.utils.interpolate(xLastLargeSlot, xAfterLastLargeSlot, transitionProgress);
911
+ opacity = gsap.utils.interpolate(1, 0, transitionProgress);
912
+ visible = true;
570
913
  }
571
914
  else {
572
- width = S;
573
- x = X_N + (p - N) * (S + gap);
915
+ // La tarjeta está completamente fuera de la pantalla a la derecha, más allá de la ranura de asomo.
916
+ const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
917
+ width = smallItemWidth;
918
+ const distanceToClear = Math.max(smallItemWidth + padding, containerWidth - xAfterLastLargeSlot);
919
+ horizontalPosition = xAfterLastLargeSlot + (normalizedPosition - largeItemCount) * distanceToClear;
574
920
  opacity = 0;
921
+ visible = false;
575
922
  }
576
923
  }
577
924
  else {
578
- // multi-browse
579
- const X_N = padding + N * (L + gap);
580
- const X_N1 = X_N + M + gap;
581
- const X_M1 = padding - gap - S;
582
- if (p <= -1) {
583
- width = S;
584
- x = X_M1 + (p - -1) * (S + gap);
925
+ // `xMediumSlot` — X position where the medium item begins (after the last large item).
926
+ const xMediumSlot = padding + largeItemCount * (largeItemWidth + gap);
927
+ // `xSmallSlot` X position where the small peeking item begins (after the medium item).
928
+ const xSmallSlot = xMediumSlot + mediumItemWidth + gap;
929
+ // `xBeforeFirstLargeSlot` X position of the peeking small item off the left edge.
930
+ const xBeforeFirstLargeSlot = -smallItemWidth;
931
+ if (normalizedPosition <= -1) {
932
+ // La tarjeta está oculta fuera de la pantalla a la izquierda.
933
+ width = smallItemWidth;
934
+ horizontalPosition = xBeforeFirstLargeSlot + (normalizedPosition - -1) * (smallItemWidth + Math.max(gap, padding));
585
935
  opacity = 0;
936
+ visible = false;
586
937
  }
587
- else if (p <= 0) {
588
- width = gsap.utils.interpolate(S, L, p + 1);
589
- x = gsap.utils.interpolate(X_M1, padding, p + 1);
590
- opacity = gsap.utils.interpolate(0, 1, p + 1);
938
+ else if (normalizedPosition <= 0) {
939
+ // La tarjeta está en transición de entrada desde la izquierda (pequeño → grande).
940
+ width = gsap.utils.interpolate(smallItemWidth, largeItemWidth, normalizedPosition + 1);
941
+ horizontalPosition = gsap.utils.interpolate(xBeforeFirstLargeSlot, padding, normalizedPosition + 1);
942
+ opacity = gsap.utils.interpolate(0, 1, normalizedPosition + 1);
943
+ visible = true;
591
944
  }
592
- else if (p <= N - 1) {
593
- width = L;
594
- x = padding + p * (L + gap);
945
+ else if (normalizedPosition <= largeItemCount - 1) {
946
+ // La tarjeta está en una de las ranuras grandes (completamente visible).
947
+ width = largeItemWidth;
948
+ horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
595
949
  opacity = 1;
950
+ visible = true;
596
951
  }
597
- else if (p <= N) {
598
- const u = p - (N - 1);
599
- const X_last = padding + (N - 1) * (L + gap);
600
- width = gsap.utils.interpolate(L, M, u);
601
- x = gsap.utils.interpolate(X_last, X_N, u);
602
- opacity = gsap.utils.interpolate(1, 0, u);
952
+ else if (normalizedPosition <= largeItemCount) {
953
+ // La tarjeta está en transición de grande a mediano (entrando a la ranura mediana).
954
+ const transitionProgress = normalizedPosition - (largeItemCount - 1);
955
+ const xLastLargeSlot = padding + (largeItemCount - 1) * (largeItemWidth + gap);
956
+ width = gsap.utils.interpolate(largeItemWidth, mediumItemWidth, transitionProgress);
957
+ horizontalPosition = gsap.utils.interpolate(xLastLargeSlot, xMediumSlot, transitionProgress);
958
+ // La opacidad se desvanece a 0 a medida que la tarjeta se encoge en la ranura mediana — el efecto de máscara.
959
+ opacity = gsap.utils.interpolate(1, 0, transitionProgress);
960
+ visible = true;
603
961
  }
604
- else if (p <= N + 1) {
605
- const u = p - N;
606
- width = gsap.utils.interpolate(M, S, u);
607
- x = gsap.utils.interpolate(X_N, X_N1, u);
608
- opacity = 0;
962
+ else if (normalizedPosition <= largeItemCount + 1) {
963
+ // La tarjeta está en la ranura mediana, en transición hacia la ranura pequeña/asomando.
964
+ const transitionProgress = normalizedPosition - largeItemCount;
965
+ width = gsap.utils.interpolate(mediumItemWidth, smallItemWidth, transitionProgress);
966
+ horizontalPosition = gsap.utils.interpolate(xMediumSlot, xSmallSlot, transitionProgress);
967
+ opacity = 0; // Ya oculta (enmascarada) una vez en la ranura mediana.
968
+ visible = true;
609
969
  }
610
970
  else {
611
- width = S;
612
- x = X_N1 + (p - (N + 1)) * (S + gap);
971
+ // La tarjeta está completamente fuera de la pantalla a la derecha.
972
+ const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
973
+ width = smallItemWidth;
974
+ const distanceToClear = Math.max(smallItemWidth + padding, containerWidth - xSmallSlot);
975
+ horizontalPosition = xSmallSlot + (normalizedPosition - (largeItemCount + 1)) * distanceToClear;
613
976
  opacity = 0;
977
+ visible = false;
614
978
  }
615
979
  }
616
- return { width, x, opacity };
980
+ return { width, x: horizontalPosition, opacity, visible };
617
981
  }
618
982
  /**
619
- * Rebuild quickSetter caches after cards change.
620
- * quickSetter avoids gsap.set() overhead for properties updated every frame.
983
+ * Reconstruye las cachés de quickSetter después de que cambian las tarjetas.
984
+ * quickSetter evita la sobrecarga de gsap.set() para las propiedades actualizadas en cada cuadro.
621
985
  */
622
986
  _buildQuickSetters() {
623
987
  if (!this._cards || this._cards.length === 0)
@@ -626,6 +990,8 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
626
990
  this._cardSetWidth = [];
627
991
  this._imgSetX = [];
628
992
  this._titleSetOpacity = [];
993
+ // Reset logical viewport state array to match original items count
994
+ this._logicalItemInViewport = new Array(this.effectiveItems.length).fill(false);
629
995
  this._cards.forEach((card) => {
630
996
  this._cardSetX.push(gsap.quickSetter(card, 'x', 'px'));
631
997
  this._cardSetWidth.push(gsap.quickSetter(card, 'width', 'px'));
@@ -635,116 +1001,415 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
635
1001
  this._titleSetOpacity.push(title ? gsap.quickSetter(title, 'opacity') : (() => { }));
636
1002
  });
637
1003
  }
638
- _applyLayout(t) {
1004
+ /**
1005
+ * Bucle de renderizado principal — aplica el diseño calculado a cada tarjeta en el DOM.
1006
+ *
1007
+ * Llamado en cada cuadro del ticker de GSAP a través de `_tick()`. Itera sobre todos los elementos físicos
1008
+ * de las tarjetas y aplica ancho, posición, paralaje y opacidad del título
1009
+ * usando quickSetters de GSAP pre-almacenados en caché para un rendimiento máximo de velocidad de fotogramas.
1010
+ *
1011
+ * **Bucle de desplazamiento infinito:**
1012
+ * Cuando `infinite=true`, la posición normalizada se calcula usando aritmética
1013
+ * modular para que las tarjetas se repitan sin problemas alrededor del centro virtual del búfer.
1014
+ * Una corrección de envoltura asegura que cada tarjeta permanezca dentro de una distancia de medio ancho de búfer
1015
+ * de la posición de desplazamiento actual.
1016
+ *
1017
+ * **Optimización fuera de pantalla:**
1018
+ * Las tarjetas que están a más de 15 posiciones fuera de la pantalla se mueven a `x = -9999` y sus
1019
+ * títulos se ocultan. Esto evita que el navegador gaste el presupuesto de composición
1020
+ * en elementos invisibles.
1021
+ *
1022
+ * @param scrollProgress - La posición de desplazamiento interpolada actual en unidades de `itemSize`.
1023
+ * Un valor de `1.0` significa que el contenedor de desplazamiento ha avanzado exactamente el ancho de un elemento + espacio.
1024
+ */
1025
+ _applyLayout(scrollProgress) {
639
1026
  const isMobile = this._isMobile;
640
1027
  const cardCount = this._cards.length;
641
- for (let i = 0; i < cardCount; i++) {
642
- const p = i - t;
643
- const layout = this._getCardLayout(p);
644
- // Use quickSetters ~3x faster than gsap.set() per call
645
- this._cardSetX[i](layout.x);
646
- this._cardSetWidth[i](layout.width);
647
- // Skip parallax on mobile to save per-frame work
1028
+ const visibleLogicalItems = new Map();
1029
+ const allLogicalItems = new Map();
1030
+ for (let cardIndex = 0; cardIndex < cardCount; cardIndex++) {
1031
+ // `normalizedPosition` representa cuántas ranuras de elementos esta tarjeta está
1032
+ // desplazada desde el punto focal actual (0 = ranura grande primaria).
1033
+ let normalizedPosition = cardIndex - scrollProgress;
1034
+ const card = this._cards[cardIndex];
1035
+ const dataIndex = Number(card.dataset.index || 0);
1036
+ const logicalIndex = this.infinite ? dataIndex % this.effectiveItems.length : dataIndex;
1037
+ // Mapear cada índice lógico a un nodo físico para garantizar que tengamos
1038
+ // una referencia DOM incluso cuando la tarjeta esté saliendo (leave event).
1039
+ if (!allLogicalItems.has(logicalIndex)) {
1040
+ allLogicalItems.set(logicalIndex, card);
1041
+ }
1042
+ if (this.infinite) {
1043
+ // Envolver la posición usando aritmética modular para mantener cada tarjeta dentro de
1044
+ // una distancia de ±medio-búfer de la posición de desplazamiento actual.
1045
+ normalizedPosition = cardIndex - (scrollProgress % cardCount);
1046
+ if (normalizedPosition > cardCount / 2)
1047
+ normalizedPosition -= cardCount;
1048
+ if (normalizedPosition < -cardCount / 2)
1049
+ normalizedPosition += cardCount;
1050
+ }
1051
+ // Omitir renderizado para tarjetas muy fuera del viewport visible para reducir
1052
+ // la carga de composición del navegador (un margen de seguridad de ~30 tarjetas alrededor de la pantalla).
1053
+ if (normalizedPosition < -15 || normalizedPosition > 15) {
1054
+ // Move the card completely off-screen and hide its title.
1055
+ this._cardSetX[cardIndex](-9999);
1056
+ this._titleSetOpacity[cardIndex](0);
1057
+ continue;
1058
+ }
1059
+ // Get the precise pixel width and position for this card at its current normalized position.
1060
+ const cardLayoutResult = this._getCardLayout(normalizedPosition);
1061
+ // Map logical visibility
1062
+ if (cardLayoutResult.visible) {
1063
+ if (!visibleLogicalItems.has(logicalIndex)) {
1064
+ visibleLogicalItems.set(logicalIndex, card);
1065
+ }
1066
+ }
1067
+ // Apply position and width via quickSetters (~3× faster than gsap.set() per call).
1068
+ this._cardSetX[cardIndex](cardLayoutResult.x);
1069
+ this._cardSetWidth[cardIndex](cardLayoutResult.width);
1070
+ // Parallax effect: shift the image slightly in the opposite direction of scroll
1071
+ // to create depth. Clamped to ±1 to prevent over-shifting at extreme positions.
1072
+ // Skipped on mobile to conserve per-frame computation budget.
648
1073
  if (!isMobile) {
649
- const clampedP = Math.max(-1, Math.min(1, p));
650
- this._imgSetX[i](clampedP * -25);
1074
+ const clampedParallaxPosition = Math.max(-1, Math.min(1, normalizedPosition));
1075
+ // Image shifts up to ±25px to create the parallax depth illusion.
1076
+ this._imgSetX[cardIndex](clampedParallaxPosition * -25);
651
1077
  }
652
- this._titleSetOpacity[i](layout.opacity);
1078
+ this._titleSetOpacity[cardIndex](cardLayoutResult.opacity);
653
1079
  }
1080
+ // Calculate diff over logical items to dispatch exact events globally without jump glitches
1081
+ for (let i = 0; i < this.effectiveItems.length; i++) {
1082
+ const isNowVisible = visibleLogicalItems.has(i);
1083
+ if (this._logicalItemInViewport[i] !== isNowVisible) {
1084
+ this._logicalItemInViewport[i] = isNowVisible;
1085
+ const eventName = isNowVisible ? 'moni-item-entered-viewport' : 'moni-item-left-viewport';
1086
+ // Usamos el nodo físico visible si entra, o el nodo físico de respaldo si sale
1087
+ const physicalCard = visibleLogicalItems.get(i) || allLogicalItems.get(i) || null;
1088
+ emitMoniEvent(this, eventName, {
1089
+ detail: { item: this.effectiveItems[i], index: i, card: physicalCard }
1090
+ });
1091
+ }
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Calcula el multiplicador necesario para rellenar visualmente el carrusel infinito.
1096
+ * Si la lista original de ítems es corta (ej: 3 ítems) pero el viewport puede
1097
+ * mostrar 5, necesitamos multiplicar la lista original para cubrir el ancho de
1098
+ * la pantalla e interactuar de forma ininterrumpida (Infinity Wrap).
1099
+ */
1100
+ get _visualCardsMultiplier() {
1101
+ if (!this.infinite || this.effectiveItems.length === 0)
1102
+ return 1;
1103
+ const C = this.effectiveItems.length;
1104
+ // We need at least ~15 cards to fill the screen seamlessly for small datasets
1105
+ return Math.max(1, Math.ceil(15 / C));
1106
+ }
1107
+ /**
1108
+ * Calcula cuántos "sets" completos de las cards originales se deben clonar
1109
+ * hacia los lados para habilitar el Infinite Scroll.
1110
+ * Usualmente es de 3 a 5 "sets" multiplicados para crear un buffer circular
1111
+ * invisible para el usuario final.
1112
+ */
1113
+ get _snapSetsCount() {
1114
+ if (!this.infinite || this.effectiveItems.length === 0)
1115
+ return 1;
1116
+ const C = this.effectiveItems.length;
1117
+ const V = C * this._visualCardsMultiplier;
1118
+ // We want enough snap items so the user NEVER reaches the end in a single fast swipe.
1119
+ // A total of ~1000 items is extremely cheap as empty divs, and provides a massive buffer.
1120
+ let K = Math.ceil(1000 / V);
1121
+ if (K % 2 === 0)
1122
+ K += 1; // Keep it odd for a perfect center
1123
+ return Math.max(3, K);
1124
+ }
1125
+ /**
1126
+ * Determina el conteo total físico de cards (`li`) que serán montados en el DOM.
1127
+ * Es el producto de `effectiveItems.length` multiplicado por `_snapSetsCount`.
1128
+ * Para carruseles finitos (no infinite), esto simplemente retorna la cantidad original.
1129
+ */
1130
+ get _snapItemsCount() {
1131
+ if (this.infinite) {
1132
+ const C = this.effectiveItems.length;
1133
+ const V_mult = this._visualCardsMultiplier;
1134
+ const K = this._snapSetsCount;
1135
+ return K * (C * V_mult);
1136
+ }
1137
+ return this.effectiveItems.length;
1138
+ }
1139
+ /**
1140
+ * Lógica de Bucle Infinito (Infinite Wrap).
1141
+ *
1142
+ * Analiza el valor actual del `scrollLeft` del contenedor. Si el usuario se
1143
+ * acerca demasiado al límite del buffer (físico izquierdo o derecho) de los ítems
1144
+ * clonados, este método reposiciona instantáneamente (saltándose animaciones/scroll-behavior)
1145
+ * el scrollLeft hacia el bloque central equivalente.
1146
+ * El usuario nunca nota el salto visual porque los ítems en esas coordenadas lucen idénticos.
1147
+ */
1148
+ _checkInfiniteWrap() {
1149
+ if (!this.infinite || this.effectiveItems.length === 0 || !this._scrollContainer)
1150
+ return;
1151
+ const C = this.effectiveItems.length;
1152
+ const V = C * this._visualCardsMultiplier;
1153
+ const K = this._snapSetsCount;
1154
+ const centerSetIndex = Math.floor(K / 2);
1155
+ // The scroll track is divided into K sets, each containing V items.
1156
+ const setWidth = V * this.itemSize;
1157
+ const currentScroll = this._scrollContainer.scrollLeft;
1158
+ const currentSet = Math.floor(currentScroll / setWidth);
1159
+ if (currentSet !== centerSetIndex) {
1160
+ const delta = (centerSetIndex - currentSet) * setWidth;
1161
+ const oldSnap = this._scrollContainer.style.scrollSnapType;
1162
+ const oldBehavior = this._scrollContainer.style.scrollBehavior;
1163
+ // Temporarily disable snap and smooth behavior so the wrap is instantaneous
1164
+ this._scrollContainer.style.scrollSnapType = 'none';
1165
+ this._scrollContainer.style.scrollBehavior = 'auto';
1166
+ this._scrollContainer.scrollLeft += delta;
1167
+ // INSTANTLY update the GSAP visual ticker so it doesn't animate the jump
1168
+ this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
1169
+ this._tickerCurrent = this._tickerTarget;
1170
+ this._applyLayout(this._tickerCurrent);
1171
+ // Restore original scroll behavior
1172
+ this._scrollContainer.style.scrollBehavior = oldBehavior;
1173
+ if (this._isDragging) {
1174
+ this._scrollLeftAtDragStart += delta;
1175
+ }
1176
+ if (oldSnap && oldSnap !== 'none') {
1177
+ // Re-enable snap on the next frame to avoid jumping
1178
+ requestAnimationFrame(() => {
1179
+ if (this._scrollContainer && !this._isDragging) {
1180
+ this._scrollContainer.style.scrollSnapType = oldSnap;
1181
+ }
1182
+ });
1183
+ }
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Inicia el temporizador de reproducción automática (Autoplay).
1188
+ * Evalúa si el usuario no está interactuando activamente (`_isDown`)
1189
+ * antes de forzar el scroll hacia la siguiente vista.
1190
+ */
1191
+ _startAutoplay() {
1192
+ this._stopAutoplay();
1193
+ if (this.autoplay && this.effectiveItems.length > 0) {
1194
+ this._autoplayTimer = setInterval(() => {
1195
+ // We removed _isInteracting to make autoplay more robust. It only pauses on active drag.
1196
+ if (!this._isDragging) {
1197
+ this._scrollNext();
1198
+ }
1199
+ }, this.autoplayInterval);
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Detiene y limpia el temporizador de reproducción automática (Autoplay).
1204
+ * Invocado durante interacciones del usuario (hover, drag) o al desmontar.
1205
+ */
1206
+ _stopAutoplay() {
1207
+ if (this._autoplayTimer) {
1208
+ clearInterval(this._autoplayTimer);
1209
+ this._autoplayTimer = null;
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Manejador de evento: `mouseenter`.
1214
+ * Expuesto por si se requiere pausar el autoplay al hacer hover (actualmente en desuso).
1215
+ */
1216
+ _handleMouseEnter() {
654
1217
  }
1218
+ /**
1219
+ * Manejador de evento: `mouseleave`.
1220
+ * Finaliza forzosamente cualquier secuencia de drag o interacción táctil en progreso
1221
+ * si el puntero abandona el área de bounding box del carrusel.
1222
+ */
1223
+ _handleMouseLeave() {
1224
+ this._handleMouseUp();
1225
+ }
1226
+ /**
1227
+ * Inicia la secuencia de arrastre (Pointer/Mouse Down).
1228
+ * - Elimina las animaciones GSAP en vuelo para evitar conflictos elásticos.
1229
+ * - Captura la posición de inicio X y resetea el acumulador de fricción y velocidad.
1230
+ * - Realiza una comprobación de Infinite Wrap preliminar por si el usuario
1231
+ * comienza a arrastrar desde el límite del buffer.
1232
+ */
655
1233
  _handleMouseDown(e) {
1234
+ // Kill any in-flight GSAP tween on the scroll container (e.g. momentum scroll
1235
+ // from a previous release) to prevent fighting with the new drag gesture.
656
1236
  gsap.killTweensOf(this._scrollContainer);
657
- this._isDown = true;
658
- this._draggedDistance = 0;
1237
+ // Check for infinite wrap before starting drag, in case the user
1238
+ // starts a new drag while near the buffer edge.
1239
+ this._checkInfiniteWrap();
1240
+ this._isDragging = true;
1241
+ this._totalDraggedDistance = 0;
1242
+ // Disable native scroll-snap and smooth behavior during drag so the
1243
+ // scroll position tracks the pointer pixel-perfectly.
659
1244
  this._scrollContainer.style.scrollBehavior = 'auto';
660
1245
  this._scrollContainer.style.scrollSnapType = 'none';
661
- this._startX = e.pageX - this._scrollContainer.offsetLeft;
662
- this._scrollLeftStart = this._scrollContainer.scrollLeft;
663
- this._velocity = 0;
664
- this._lastX = e.pageX;
665
- this._lastTime = performance.now();
1246
+ // Capture the starting position relative to the scroll container's left edge.
1247
+ this._dragStartX = e.pageX - this._scrollContainer.offsetLeft;
1248
+ this._scrollLeftAtDragStart = this._scrollContainer.scrollLeft;
1249
+ // Reset velocity tracking for fresh momentum calculation.
1250
+ this._pointerVelocity = 0;
1251
+ this._lastPointerX = e.pageX;
1252
+ this._lastPointerTime = performance.now();
666
1253
  }
1254
+ /**
1255
+ * Maneja el evento de movimiento del puntero durante un arrastre (Drag).
1256
+ *
1257
+ * @logic
1258
+ * 1. Calcula el delta (`walk`) respecto a la posición original X.
1259
+ * 2. Si excede el threshold de arrastre (3px), comienza a inyectar el scroll.
1260
+ * 3. En carruseles finitos (no infinite), si el usuario sobrepasa el inicio o el final,
1261
+ * aplica una fricción exponencial al `walk` para simular la "resistencia elástica" (Overscroll).
1262
+ * 4. Captura la velocidad (`_velocity`) calculando el delta dividido por el lapso de tiempo.
1263
+ */
667
1264
  _handleMouseMove(e) {
668
- if (!this._isDown)
1265
+ if (!this._isDragging)
669
1266
  return;
670
1267
  e.preventDefault();
671
- const currentX = e.pageX;
672
- const x = currentX - this._scrollContainer.offsetLeft;
673
- this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x) * 1.25;
1268
+ const currentPointerX = e.pageX;
1269
+ // `pointerOffsetInContainer` is the pointer's X relative to the scroll container's left edge.
1270
+ const pointerOffsetInContainer = currentPointerX - this._scrollContainer.offsetLeft;
1271
+ // Apply a 1.25× multiplier for a faster-feeling drag that closely matches
1272
+ // the M3 Expressive motion spec for carousels.
1273
+ this._scrollContainer.scrollLeft = this._scrollLeftAtDragStart + (this._dragStartX - pointerOffsetInContainer) * 1.25;
1274
+ // Calculate instantaneous velocity (px/ms) for the momentum/inertia calculation on release.
674
1275
  const currentTime = performance.now();
675
- const dt = currentTime - this._lastTime;
676
- if (dt > 0) {
677
- this._velocity = (currentX - this._lastX) / dt;
1276
+ const elapsedMs = currentTime - this._lastPointerTime;
1277
+ if (elapsedMs > 0) {
1278
+ this._pointerVelocity = (currentPointerX - this._lastPointerX) / elapsedMs;
678
1279
  }
679
- this._draggedDistance += Math.abs(currentX - this._lastX);
680
- this._lastX = currentX;
681
- this._lastTime = currentTime;
1280
+ // Accumulate total distance to differentiate deliberate swipes from accidental click-drags.
1281
+ this._totalDraggedDistance += Math.abs(currentPointerX - this._lastPointerX);
1282
+ this._lastPointerX = currentPointerX;
1283
+ this._lastPointerTime = currentTime;
682
1284
  }
1285
+ /**
1286
+ * Finaliza la secuencia de arrastre (Pointer/Mouse Up).
1287
+ *
1288
+ * @logic
1289
+ * Restaura el "Scroll Snap" que fue desactivado durante el drag (para permitir un
1290
+ * arrastre libre píxel por píxel). Adicionalmente, si el arrastre acumuló suficiente
1291
+ * velocidad (Inercia), inyecta esa inercia desplazando imperativamente el contenedor.
1292
+ */
683
1293
  _handleMouseUp() {
684
- if (!this._isDown)
1294
+ if (!this._isDragging)
685
1295
  return;
686
- this._isDown = false;
1296
+ this._isDragging = false;
1297
+ // If the last pointer event was more than 100ms ago, the user paused
1298
+ // before releasing — treat it as a zero-velocity drop (no momentum).
687
1299
  const currentTime = performance.now();
688
- if (currentTime - this._lastTime > 100) {
689
- this._velocity = 0;
1300
+ if (currentTime - this._lastPointerTime > 100) {
1301
+ this._pointerVelocity = 0;
1302
+ }
1303
+ // ── Momentum / Inertia calculation ───────────────────────────────────
1304
+ // Dynamic momentum multiplier: faster swipes multiply further.
1305
+ // Base of 350px + an additional 300px per px/ms of velocity.
1306
+ const momentumMultiplier = 350 + Math.abs(this._pointerVelocity) * 300;
1307
+ // Project the target scroll position based on the velocity vector.
1308
+ let projectedScrollLeft = this._scrollContainer.scrollLeft - this._pointerVelocity * momentumMultiplier;
1309
+ // Cap the throw distance at 15 items to prevent the user from flying past
1310
+ // the infinite scroll buffer boundaries in a single fast swipe.
1311
+ const maxMomentumThrowDistance = 15 * this.itemSize;
1312
+ if (projectedScrollLeft < this._scrollContainer.scrollLeft - maxMomentumThrowDistance) {
1313
+ projectedScrollLeft = this._scrollContainer.scrollLeft - maxMomentumThrowDistance;
1314
+ }
1315
+ else if (projectedScrollLeft > this._scrollContainer.scrollLeft + maxMomentumThrowDistance) {
1316
+ projectedScrollLeft = this._scrollContainer.scrollLeft + maxMomentumThrowDistance;
690
1317
  }
691
- // Dynamic momentum: stronger swipes have a much higher multiplier
692
- const momentumMultiplier = 350 + Math.abs(this._velocity) * 300;
693
- const projectedScrollLeft = this._scrollContainer.scrollLeft - this._velocity * momentumMultiplier;
694
1318
  if (this.layout !== 'uncontained') {
1319
+ // Disable snap while GSAP animates to the snap point to allow smooth fly-through.
695
1320
  this._scrollContainer.style.scrollSnapType = 'none';
696
- let targetIndex = Math.round(projectedScrollLeft / this.itemSize);
697
- targetIndex = Math.max(0, Math.min(this.effectiveItems.length - 1, targetIndex));
698
- const snapPoint = targetIndex * this.itemSize;
699
- // Dynamic duration: longer distance = more time, max 0.85s for faster feeling
700
- const currentIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize);
701
- const cardsSkipped = Math.abs(targetIndex - currentIndex);
702
- const duration = Math.min(0.85, 0.25 + cardsSkipped * 0.04);
1321
+ // Snap to the nearest item index in the direction of momentum.
1322
+ let targetSnapIndex = Math.round(projectedScrollLeft / this.itemSize);
1323
+ const maxSnapIndex = this.effectiveItems.length > 0 ? this._snapItemsCount - 1 : 0;
1324
+ // Clamp to valid index range.
1325
+ targetSnapIndex = Math.max(0, Math.min(maxSnapIndex, targetSnapIndex));
1326
+ const snapTargetScrollLeft = targetSnapIndex * this.itemSize;
1327
+ // Dynamic duration: longer distance = more time, capped at 0.85s.
1328
+ // This gives a natural "lighter = faster, heavier = slower" momentum feel.
1329
+ const currentSnapIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize);
1330
+ const cardsTraveled = Math.abs(targetSnapIndex - currentSnapIndex);
1331
+ const snapDuration = Math.min(0.85, 0.25 + cardsTraveled * 0.04);
703
1332
  this._scrollContainer.style.scrollBehavior = 'auto';
1333
+ // Animate the scroll to the snap point using GSAP for precise easing control.
704
1334
  gsap.to(this._scrollContainer, {
705
- scrollLeft: snapPoint,
706
- duration: duration,
707
- ease: 'power3.out',
1335
+ scrollLeft: snapTargetScrollLeft,
1336
+ duration: snapDuration,
1337
+ ease: 'power3.out', // Decelerating ease matching M3 Expressive spring curves.
708
1338
  onComplete: () => {
709
- if (!this._isDown && this._scrollContainer) {
1339
+ // Restore native CSS scroll-snap after the animation completes
1340
+ // so keyboard and touch-based scrolling can snap again.
1341
+ if (!this._isDragging && this._scrollContainer) {
710
1342
  this._scrollContainer.style.scrollSnapType = 'x mandatory';
711
1343
  }
712
1344
  }
713
1345
  });
714
1346
  }
715
1347
  }
1348
+ /**
1349
+ * Inicia la secuencia de arrastre táctil (Touch Start).
1350
+ * Funciona análogamente a `_handleMouseDown`: detiene animaciones GSAP, evalúa el Infinite Wrap
1351
+ * y captura las coordenadas táctiles iniciales (`e.touches[0]`).
1352
+ */
716
1353
  _handleTouchStart(e) {
1354
+ // Mirror the mouse-down setup: kill flying animations, check for wrap,
1355
+ // then capture the starting touch coordinates.
717
1356
  gsap.killTweensOf(this._scrollContainer);
718
- this._isDown = true;
719
- this._draggedDistance = 0;
1357
+ this._checkInfiniteWrap();
1358
+ this._isDragging = true;
1359
+ this._totalDraggedDistance = 0;
720
1360
  this._scrollContainer.style.scrollBehavior = 'auto';
721
1361
  this._scrollContainer.style.scrollSnapType = 'none';
722
- const touch = e.touches[0];
723
- this._startX = touch.pageX - this._scrollContainer.offsetLeft;
724
- this._scrollLeftStart = this._scrollContainer.scrollLeft;
725
- this._velocity = 0;
726
- this._lastX = touch.pageX;
727
- this._lastTime = performance.now();
1362
+ // `e.touches[0]` is the first active touch point on the screen.
1363
+ const primaryTouch = e.touches[0];
1364
+ this._dragStartX = primaryTouch.pageX - this._scrollContainer.offsetLeft;
1365
+ this._scrollLeftAtDragStart = this._scrollContainer.scrollLeft;
1366
+ this._pointerVelocity = 0;
1367
+ this._lastPointerX = primaryTouch.pageX;
1368
+ this._lastPointerTime = performance.now();
728
1369
  }
1370
+ /**
1371
+ * Maneja el evento de movimiento táctil (Touch Move).
1372
+ * Actualiza el `scrollLeft` del contenedor imperativamente según el delta de arrastre.
1373
+ * Calcula la velocidad del dedo para la simulación de inercia posterior.
1374
+ */
729
1375
  _handleTouchMove(e) {
730
- if (!this._isDown)
1376
+ if (!this._isDragging)
731
1377
  return;
732
- const touch = e.touches[0];
733
- const currentX = touch.pageX;
734
- const x = currentX - this._scrollContainer.offsetLeft;
735
- this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x);
1378
+ const primaryTouch = e.touches[0];
1379
+ const currentTouchX = primaryTouch.pageX;
1380
+ // Compute the touch offset relative to the scroll container's left edge.
1381
+ const touchOffsetInContainer = currentTouchX - this._scrollContainer.offsetLeft;
1382
+ // Update scroll position directly (no multiplier for touch — native 1:1 feel).
1383
+ this._scrollContainer.scrollLeft = this._scrollLeftAtDragStart + (this._dragStartX - touchOffsetInContainer);
1384
+ // Track velocity for momentum calculation on touch end.
736
1385
  const currentTime = performance.now();
737
- const dt = currentTime - this._lastTime;
738
- if (dt > 0) {
739
- this._velocity = (currentX - this._lastX) / dt;
1386
+ const elapsedMs = currentTime - this._lastPointerTime;
1387
+ if (elapsedMs > 0) {
1388
+ this._pointerVelocity = (currentTouchX - this._lastPointerX) / elapsedMs;
740
1389
  }
741
- this._draggedDistance += Math.abs(currentX - this._lastX);
742
- this._lastX = currentX;
743
- this._lastTime = currentTime;
1390
+ this._totalDraggedDistance += Math.abs(currentTouchX - this._lastPointerX);
1391
+ this._lastPointerX = currentTouchX;
1392
+ this._lastPointerTime = currentTime;
744
1393
  }
1394
+ /**
1395
+ * Finaliza la secuencia de arrastre táctil (Touch End).
1396
+ * Delega en `_handleMouseUp()` para aplicar inercia y restaurar el Snap.
1397
+ */
745
1398
  _handleTouchEnd() {
746
1399
  this._handleMouseUp();
747
1400
  }
1401
+ /**
1402
+ * Manejador del evento Scroll/Wheel.
1403
+ *
1404
+ * @logic
1405
+ * 1. Filtra para procesar únicamente el scroll vertical (`deltaY > deltaX`), típico de ruedas de ratón.
1406
+ * 2. Si el usuario está en los bordes extremos del carrusel, permite que el evento de scroll
1407
+ * haga bubble hacia arriba para hacer scroll a la página completa, logrando una interacción fluida.
1408
+ * 3. Si no, previene el scroll de la página (`e.preventDefault`) y lo convierte en scroll horizontal
1409
+ * para el carrusel.
1410
+ * 4. Desactiva temporalmente el "CSS Scroll Snap" para que el movimiento sea suave, restaurándolo
1411
+ * tras 150ms de inactividad (`_wheelTimeout`).
1412
+ */
748
1413
  _handleWheel(e) {
749
1414
  // Solo interceptar scroll vertical puro (rueda de mouse típica)
750
1415
  if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
@@ -770,45 +1435,85 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
770
1435
  if (this._wheelTimeout)
771
1436
  clearTimeout(this._wheelTimeout);
772
1437
  this._wheelTimeout = setTimeout(() => {
773
- if (this.layout !== 'uncontained') {
1438
+ // Handle uncontained snapping explicitly
1439
+ if (this.layout !== 'uncontained' && !this._isDragging) {
1440
+ // Fallback to CSS snap if not uncontained and not dragging
774
1441
  this._scrollContainer.style.scrollSnapType = 'x mandatory';
775
1442
  }
776
1443
  }, 150);
777
1444
  }
778
1445
  }
779
- _handleCardClick(e, item, index) {
780
- if (this._draggedDistance > 8) {
1446
+ /**
1447
+ * Maneja los eventos de clic en las tarjetas individuales del carrusel.
1448
+ *
1449
+ * Distingue entre toques/clics genuinos y eventos accidentales de levantar el puntero
1450
+ * que ocurrieron después de un gesto de arrastre. Si la distancia total arrastrada excede
1451
+ * los 8px (el umbral mínimo de desplazamiento intencional de M3), el clic se suprime
1452
+ * para evitar una navegación involuntaria o el disparo de acciones.
1453
+ *
1454
+ * Despacha `'item-click'` con `{ item, index }` para que los consumidores puedan
1455
+ * responder a la selección de la tarjeta (ej. navegar a una página de detalles).
1456
+ *
1457
+ * @param e - El evento de clic del elemento de la tarjeta.
1458
+ * @param item - Los datos `CarouselItem` asociados con la tarjeta clicada.
1459
+ * @param cardDomIndex - El índice de la tarjeta en el DOM renderizado (puede diferir
1460
+ * del índice lógico en modo infinito debido a la clonación de elementos).
1461
+ */
1462
+ _handleCardClick(e, item, cardDomIndex) {
1463
+ // Suppress click if the gesture traveled more than 8px — it was a swipe, not a tap.
1464
+ if (this._totalDraggedDistance > 8) {
781
1465
  e.preventDefault();
782
1466
  e.stopPropagation();
783
1467
  return;
784
1468
  }
1469
+ // In infinite mode, the DOM index may be a multiple of the original list length.
1470
+ // Modulo maps it back to the logical item index.
1471
+ const logicalItemIndex = cardDomIndex % this.effectiveItems.length;
785
1472
  this.dispatchEvent(new CustomEvent('item-click', {
786
- detail: { item, index },
787
1473
  bubbles: true,
788
- composed: true
1474
+ composed: true,
1475
+ detail: { item, index: logicalItemIndex }
789
1476
  }));
1477
+ emitMoniEvent(this, 'moni-item-click', {
1478
+ detail: { item, index: logicalItemIndex }
1479
+ });
790
1480
  }
1481
+ /**
1482
+ * Acción Imperativa: Scroll hacia el elemento anterior.
1483
+ * Desplaza el contenedor una distancia equivalente al tamaño exacto de un ítem `L` (Large) + gap.
1484
+ * Restaura el comportamiento `smooth` de CSS nativo.
1485
+ */
791
1486
  _scrollPrevious() {
792
- const target = Math.max(0, this._scrollContainer.scrollLeft - this.itemSize);
1487
+ const target = this._scrollContainer.scrollLeft - this.itemSize;
793
1488
  this._scrollContainer.style.scrollBehavior = 'smooth';
794
1489
  this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
795
1490
  }
1491
+ /**
1492
+ * Acción Imperativa: Scroll hacia el elemento siguiente.
1493
+ * Desplaza el contenedor una distancia equivalente al tamaño exacto de un ítem `L` (Large) + gap.
1494
+ */
796
1495
  _scrollNext() {
797
- const maxScroll = this._scrollContainer.scrollWidth - this._scrollContainer.clientWidth;
798
- const target = Math.min(maxScroll, this._scrollContainer.scrollLeft + this.itemSize);
1496
+ const target = this._scrollContainer.scrollLeft + this.itemSize;
799
1497
  this._scrollContainer.style.scrollBehavior = 'smooth';
800
1498
  this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
801
1499
  }
1500
+ /**
1501
+ * Manejador del botón o enlace "Mostrar Todo" (Show All).
1502
+ * Intercepta clics en enlaces nulos (`#`) y despacha el evento CustomEvent `show-all-click`.
1503
+ */
802
1504
  _handleShowAllClick(e) {
803
1505
  const target = e.target;
804
1506
  if (target.tagName.toLowerCase() === 'a' && target.getAttribute('href') === '#') {
805
1507
  e.preventDefault();
806
1508
  }
807
- this.dispatchEvent(new CustomEvent('show-all-click', {
808
- bubbles: true,
809
- composed: true
810
- }));
1509
+ this.dispatchEvent(new CustomEvent('show-all-click', { bubbles: true, composed: true }));
1510
+ emitMoniEvent(this, 'moni-show-all-click');
811
1511
  }
1512
+ /**
1513
+ * Hook invocado por Lit cuando el contenido del slot `"show-all"` muta en el Light DOM.
1514
+ * Detecta si hay contenido proyectado para ocultar/mostrar la interfaz correspondiente,
1515
+ * proveyendo un fallback manual para entornos DOM limitados (como JSDOM en test unitarios).
1516
+ */
812
1517
  _handleShowAllSlotChange(e) {
813
1518
  const slot = e.target;
814
1519
  let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : [];
@@ -820,6 +1525,15 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
820
1525
  }
821
1526
  this._hasSlottedShowAll = nodes.length > 0;
822
1527
  }
1528
+ /**
1529
+ * Hook invocado por Lit cuando los hijos directos proyectados en el `<slot>` por defecto cambian.
1530
+ *
1531
+ * @logic
1532
+ * Parsea el Light DOM proporcionado por el usuario (ej: lista de `<img>` o `<a><img></a>`).
1533
+ * Extrae los atributos `src`, `title`, y `href` de esos nodos nativos y los convierte en
1534
+ * un array estructurado interno de objetos `CarouselItem`, que el componente usará
1535
+ * subsecuentemente para renderizar su propio DOM Shadow infinito.
1536
+ */
823
1537
  _handleSlotChange(e) {
824
1538
  const slot = e.target;
825
1539
  let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : [];
@@ -859,41 +1573,95 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
859
1573
  }
860
1574
  void this.updateComplete.then(() => {
861
1575
  this.updateLayout(true);
1576
+ emitMoniEvent(this, 'moni-slot-items-changed', { detail: { items: this._slottedItems } });
862
1577
  });
863
1578
  }
1579
+ /**
1580
+ * Ensambla la estructura del Shadow DOM del carrusel para cada ciclo de renderizado de Lit.
1581
+ *
1582
+ * **Extracción de `computedLayout`:**
1583
+ * `largeItemWidth`, `mediumItemWidth` y `smallItemWidth` se desestructuran
1584
+ * del getter `computedLayout` (que devuelve tamaños según la especificación M3 basados en
1585
+ * el ancho del contenedor a través de `_containerWidth`). Se inyectan como propiedades personalizadas
1586
+ * CSS en la pista para que el CSS de tamaño de ranura pueda referenciarlos.
1587
+ *
1588
+ * **Puerta `showHeader`:**
1589
+ * El elemento `.header` solo se renderiza cuando hay algo que mostrar:
1590
+ * un `headerText`, un atributo `showAll`, o un elemento `[slot="show-all"]`.
1591
+ * Esto evita un espaciador de encabezado vacío cuando el carrusel no tiene fila de título.
1592
+ *
1593
+ * **Cálculo de `rightPadding`:**
1594
+ * Asegura que la última tarjeta pueda desplazarse a ras de la posición focal del elemento grande:
1595
+ * - Diseño `hero`: sin relleno adicional (el elemento derecho que asoma proporciona espacio natural).
1596
+ * - Diseños de ancho completo / lista: también relleno mínimo.
1597
+ * - Todos los demás diseños: `128px` (≈ ancho de la tarjeta pequeña) para que la última tarjeta pueda
1598
+ * alcanzar la ranura de enfoque principal sin que el usuario se desplace demasiado.
1599
+ *
1600
+ * **Ciclo de vida del ticker de GSAP:**
1601
+ * El ticker de GSAP (`gsap.ticker.add(_tick)`) se inicia en `firstUpdated()`
1602
+ * y se detiene en `disconnectedCallback()`. Este método de renderizado NO
1603
+ * interactúa con el ticker; el ticker impulsa las actualizaciones de posición por cuadro
1604
+ * independientemente del ciclo de actualización reactiva de Lit.
1605
+ */
864
1606
  render() {
865
- const { L, M, S } = this.computedLayout;
1607
+ const { largeItemWidth, mediumItemWidth, smallItemWidth } = this.computedLayout;
1608
+ // Determine whether there are items to render (from either source).
866
1609
  const hasItems = this.effectiveItems && this.effectiveItems.length > 0;
1610
+ // Check for a slotted "Show All" button or the declarative `show-all` attribute.
867
1611
  const hasSlottedShowAll = this._hasSlottedShowAll || !!this.querySelector('[slot="show-all"]');
1612
+ // Only render the header section if there is something to display in it.
868
1613
  const showHeader = this.headerText || this.showAll || hasSlottedShowAll;
1614
+ // `rightPadding` determines the trailing space in the scroll track so the
1615
+ // last item can scroll all the way to the primary (leftmost large) focus slot.
869
1616
  let rightPadding = 128;
870
1617
  if (this.layout === 'hero') {
871
- rightPadding = S + this.gap;
1618
+ // Hero: the peeking item on the right already provides natural trailing space.
1619
+ rightPadding = smallItemWidth + this.gap;
872
1620
  }
873
1621
  else if (this.layout === 'uncontained') {
1622
+ // Uncontained: minimal trailing padding; items bleed off the edge.
874
1623
  rightPadding = 16;
875
1624
  }
876
1625
  else {
877
- rightPadding = M + S + 2 * this.gap;
1626
+ // Multi-browse: both the medium and small slots provide the trailing buffer.
1627
+ rightPadding = mediumItemWidth + smallItemWidth + 2 * this.gap;
878
1628
  }
879
1629
  // Ensure the scroll track is long enough to allow the last item to scroll to the start (p = 0 focus).
880
1630
  // Safety margin of 400px avoids fractional rounding/zoom-scale snapping blockages in Firefox.
881
- const W_c = this._containerWidth || this.getBoundingClientRect().width || 360;
882
- const minRightPaddingNeeded = W_c - L - this.padding + 400;
883
- if (rightPadding < minRightPaddingNeeded) {
884
- rightPadding = minRightPaddingNeeded;
1631
+ // Measure the container width for the scroll track overflow calculation.
1632
+ const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
1633
+ // Minimum trailing padding needed so the last item can scroll to position 0 (primary focus).
1634
+ // The +400px safety margin prevents fractional zoom-scale snapping blockages in Firefox.
1635
+ const minRequiredRightPadding = containerWidth - largeItemWidth - this.padding + 400;
1636
+ if (rightPadding < minRequiredRightPadding) {
1637
+ rightPadding = minRequiredRightPadding;
1638
+ }
1639
+ // Total number of snap items in the track (includes cloned sets for infinite mode).
1640
+ let snapItemsCount = hasItems ? this._snapItemsCount : 0;
1641
+ // `visualItems` is the array of items actually rendered as card elements in the Shadow DOM.
1642
+ // For infinite mode, this is a repeated version of `effectiveItems` scaled by `_visualCardsMultiplier`.
1643
+ let visualItems = this.effectiveItems;
1644
+ if (this.infinite && hasItems) {
1645
+ const visualMultiplier = this._visualCardsMultiplier;
1646
+ visualItems = [];
1647
+ // Concatenate the original item list `visualMultiplier` times to create
1648
+ // enough cards to fill the visual viewport without visible gaps.
1649
+ for (let repeatIndex = 0; repeatIndex < visualMultiplier; repeatIndex++) {
1650
+ visualItems = visualItems.concat(this.effectiveItems);
1651
+ }
885
1652
  }
1653
+ // Total pixel width of the snap track (sum of all item widths + all gaps between them).
886
1654
  const trackWidth = hasItems
887
- ? this.effectiveItems.length * L + (this.effectiveItems.length - 1) * this.gap
1655
+ ? snapItemsCount * largeItemWidth + (snapItemsCount - 1) * this.gap
888
1656
  : 0;
889
1657
  const isScrollable = hasItems && this._scrollContainer &&
890
1658
  (this._scrollContainer.scrollWidth > this._scrollContainer.clientWidth);
891
- const showPrevArrow = isScrollable && this._scrollContainer.scrollLeft > 5;
892
- const showNextArrow = isScrollable &&
893
- (this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5);
1659
+ const showPrevArrow = this.infinite || (isScrollable && this._scrollContainer.scrollLeft > 5);
1660
+ const showNextArrow = this.infinite || (isScrollable &&
1661
+ (this._scrollContainer.scrollWidth - this._scrollContainer.scrollLeft - this._scrollContainer.clientWidth > 5));
894
1662
  return html `
895
1663
  <div class="carousel-container" style="
896
- --carousel-snap-width: ${L}px;
1664
+ --carousel-snap-width: ${largeItemWidth}px;
897
1665
  --carousel-gap: ${this.gap}px;
898
1666
  --carousel-padding: ${this.padding}px;
899
1667
  --carousel-right-padding: ${rightPadding}px;
@@ -937,34 +1705,36 @@ let MoniCarousel = class MoniCarousel extends MoniElement {
937
1705
  @mousedown=${this._handleMouseDown}
938
1706
  @mousemove=${this._handleMouseMove}
939
1707
  @mouseup=${this._handleMouseUp}
940
- @mouseleave=${this._handleMouseUp}
1708
+ @mouseenter=${this._handleMouseEnter}
1709
+ @mouseleave=${this._handleMouseLeave}
941
1710
  @touchstart=${this._handleTouchStart}
942
1711
  @touchmove=${this._handleTouchMove}
943
1712
  @touchend=${this._handleTouchEnd}
944
1713
  @wheel=${this._handleWheel}
945
1714
  >
946
1715
  <div class="snap-track">
947
- ${hasItems ? this.effectiveItems.map(() => html `<div class="snap-item"></div>`) : ''}
1716
+ ${hasItems ? Array.from({ length: snapItemsCount }).map(() => html `<div class="snap-item"></div>`) : ''}
948
1717
  </div>
949
1718
  <div class="visual-track">
950
1719
  ${hasItems
951
- ? this.effectiveItems.map((item, idx) => html `
1720
+ ? visualItems.map((item, idx) => html `
952
1721
  <div
953
1722
  class="card"
1723
+ data-index="${idx}"
954
1724
  @click=${(e) => this._handleCardClick(e, item, idx)}
955
1725
  >
956
1726
  ${item.href
957
1727
  ? html `
958
- <a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;">
1728
+ <a href="${item.href}" target="${item.target || '_self'}" style="text-decoration:none; color:inherit;" draggable="false">
959
1729
  <div class="img-parallax-container">
960
- <img src="${item.img}" alt="${item.title}" draggable="false" />
1730
+ <img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
961
1731
  </div>
962
1732
  <h2 class="card-title">${item.title}</h2>
963
1733
  </a>
964
1734
  `
965
1735
  : html `
966
1736
  <div class="img-parallax-container">
967
- <img src="${item.img}" alt="${item.title}" draggable="false" />
1737
+ <img src="${item.img}" alt="${item.title}" draggable="false" loading="lazy" />
968
1738
  </div>
969
1739
  <h2 class="card-title">${item.title}</h2>
970
1740
  `}
@@ -1029,6 +1799,15 @@ __decorate([
1029
1799
  __decorate([
1030
1800
  property({ type: Boolean, attribute: 'hide-nav' })
1031
1801
  ], MoniCarousel.prototype, "hideNav", void 0);
1802
+ __decorate([
1803
+ property({ type: Boolean, reflect: true })
1804
+ ], MoniCarousel.prototype, "infinite", void 0);
1805
+ __decorate([
1806
+ property({ type: Boolean, reflect: true })
1807
+ ], MoniCarousel.prototype, "autoplay", void 0);
1808
+ __decorate([
1809
+ property({ type: Number, attribute: 'autoplay-interval' })
1810
+ ], MoniCarousel.prototype, "autoplayInterval", void 0);
1032
1811
  __decorate([
1033
1812
  state()
1034
1813
  ], MoniCarousel.prototype, "_containerWidth", void 0);