# Plan de integración de Embla Carousel

> **Objetivo:** dejar de mantener a mano la física de scroll/swipe/snap del
> `carousel` (y de futuros componentes con desplazamiento) adoptando
> [`embla-carousel`](https://www.embla-carousel.com/) como **motor headless**,
> respetando el _headless contract_ de `@42/core`.
>
> **Estado:** Fase 0 ✅ y Fase 1 ✅ completadas — `carousel` migrado a Embla.
> Fase 2 (otros componentes) pendiente.
> **Última revisión:** 2026-06-26

---

## 1. Por qué Embla

- **Headless por diseño:** Embla provee el motor (drag con física, snap points,
  loop, resize, eje horizontal/vertical) y **cero UI**: nosotros seguimos
  poniendo flechas, dots, ARIA y estilos. Encaja con nuestro contrato.
- **Ligero y sin dependencias:** core ~4 KB gzip, arquitectura por plugins
  (`embla-carousel-autoplay`, etc.) → solo pagás el peso de lo que usás.
- **TypeScript + MIT**, muy mantenida.
- **Mismo patrón que ya usamos:** igual que `@floating-ui/dom` para
  `tooltip`/`dropdown` y `@atlaskit/pragmatic-drag-and-drop` para `kanban`,
  Embla sería una dep real **solo donde se usa** y **externalizada** en el build.

### Qué nos quita de encima (lo caro de mantener a mano)

El `carousel` actual (`packages/core/carousel/carousel.ts`, ~355 líneas) es
_index-based_: mueve el track con `transform: translateX(calc(var(--c42-carousel-index) * -100%))`.
No tiene (y son justamente las partes frágiles):

- Física de arrastre real (momentum/inercia, seguir el dedo, rubber-banding).
- Snap points, slides de ancho variable, varios slides por vista.
- Recalcular en `resize`, RTL, free-scroll.

---

## 2. Estrategia: envolver Embla, no exponerlo

Mantener nuestra clase `Carousel` como **cáscara headless** y delegar el motor a
Embla. La API pública y los eventos **no cambian** para el consumidor.

| Responsabilidad | Dueño |
|---|---|
| Descubrir partes por `data-c42-carousel-*` | `Carousel` (nuestro) |
| ARIA (`role`, `aria-roledescription`, `aria-hidden`), dots, prev/next | `Carousel` |
| `CustomEvent` tipados (`carousel:change`), `on()`, `destroy()` | `Carousel` |
| `data-active` / `data-state` reflejado en DOM | `Carousel` |
| Drag/swipe con física, snap, loop, resize, eje | **Embla** |

Mapeo de API → Embla:

| Nuestro método | Embla |
|---|---|
| `next()` | `emblaApi.scrollNext()` |
| `prev()` | `emblaApi.scrollPrev()` |
| `goTo(i)` | `emblaApi.scrollTo(i)` |
| `index` (getter) | `emblaApi.selectedScrollSnap()` |
| evento `carousel:change` | `emblaApi.on('select', …)` |
| `destroy()` | `emblaApi.destroy()` + nuestros cleanups |
| `play()` / `pause()` | plugin `embla-carousel-autoplay` (`autoplay.play()/.stop()`) |

> **Wrapper compartido:** crear `packages/core/shared/embla.ts` que centralice
> la creación de la instancia Embla y normalice opciones (axis, loop, align,
> dragFree, plugins). Así todos los componentes que usen Embla consumen un único
> punto y no repetimos wiring.

---

## 3. Cambios de build (checklist de wiring)

Embla se trata como dep externalizada, igual que floating-ui:

- [ ] `pnpm --filter @42/core add embla-carousel` (+ `embla-carousel-autoplay`
      si migramos autoplay con plugin). Versión **pineada**.
- [ ] `packages/core/vite.config.ts` → añadir a `rollupOptions.external` el
      patrón `/^embla-carousel/`.
- [ ] No requiere nueva entry (el subpath `carousel/index` ya existe).
- [ ] `package.json` → mover/registrar `embla-carousel` en `dependencies`.
- [ ] Para componentes NUEVOS que usen Embla: registrar entry en
      `vite.config.ts`, `exports` + `style.css` en `package.json`,
      `copy-assets.mjs` y `packages/core/index.ts` (según AGENTS.md).

---

## 4. Fases de implementación

### Fase 0 — Spike (1 PR pequeño)
- [ ] Añadir la dep y externalización.
- [ ] Prototipo del wrapper `shared/embla.ts`.
- [ ] Branch de prueba migrando solo el movimiento del `carousel` (sin tocar
      API pública) para medir el diff real y el peso del bundle.

### Fase 1 — Migrar `carousel` al motor Embla
- [ ] Reescribir `carousel.ts` para delegar movimiento a Embla, conservando:
      ARIA, dots, prev/next, `carousel:change`, `on()`, `destroy()`, getters.
- [ ] Reescribir `carousel.css`: pasar de `--c42-carousel-index` + `transform`
      al layout que espera Embla (viewport con `overflow`, track `display:flex`,
      slides con `flex: 0 0 …`). Mantener selectores `data-c42-carousel-*`.
- [ ] Nuevas opciones en `carousel.types.ts`: `align`, `slidesToScroll`,
      `dragFree`, `axis` ('x' | 'y'), manteniendo las actuales (`loop`,
      `autoplay`, etc.) mapeadas a Embla/plugins.
- [ ] Migrar `carousel.test.ts`: los tests que asumen el índice en la custom
      property pasan a verificar **estado/eventos** (no píxeles), consistente con
      la regla "tests layout-agnostic, sin asserts de posición". En jsdom no hay
      layout real → cubrir API (`next/prev/goTo`, eventos, ARIA, destroy) y
      mockear/omitir lo que dependa de medición física.
- [ ] Actualizar story `stories/Carousel.stories.ts` y la doc
      `docs/llm/reference/_fragments/carousel.md` (+ `pnpm docs:generate`).

### Fase 2 — Extender a otros componentes (ver §5)
- [ ] Un PR por componente, reutilizando `shared/embla.ts`.

### Verificación (cada fase)
- [ ] `pnpm test && pnpm lint && pnpm build && pnpm docs:check`
- [ ] `pnpm build-storybook` para cambios de estilo/story.

---

## 5. Otros componentes que pueden aprovechar Embla

Embla brilla en **scroll con snap** y soporta **eje vertical** (`axis: 'y'`) y
**free-scroll** — esto habilita los _wheel/drum pickers_ tipo iOS (la idea del
"clock" que mencionaste).

| Componente | Uso de Embla | Notas |
|---|---|---|
| **carousel** | Motor principal | Fase 1. |
| **gallery** | Swipe entre slides / lightbox | Reemplaza swipe manual. |
| **image-viewer** | Swipe entre imágenes | Buen encaje táctil. |
| **time-picker** ⏰ | **Wheel picker vertical** (horas/minutos/AM-PM) | El "clock": `axis:'y'` + snap por ítem. Modo nuevo, opt-in, sin romper el panel actual. |
| **date-picker / calendar** | Wheel picker día/mes/año (modo móvil iOS) | Misma técnica vertical. |

> **Fuera de alcance:** `stepper` y `tabs` quedan descartados — el scroll/snap no
> aporta valor suficiente frente a su layout actual.

> **Patrón recomendado para wheel pickers:** crear un helper
> `WheelPicker` sobre `shared/embla.ts` (`axis:'y'`, `dragFree:false`,
> `containScroll`, `loop` opcional) que emita el ítem seleccionado vía
> `on('select')`. `time-picker` y `date-picker` lo consumen para un modo
> "drum/clock" sin reescribir su lógica de valor.

### Orden sugerido tras el carousel
1. `time-picker` (wheel/clock) — mayor valor diferencial y valida el patrón vertical.
2. `gallery` + `image-viewer` — swipe, reutiliza casi tal cual el wrapper del carousel.
3. `date-picker` wheel mode.

---

## 6. Riesgos y decisiones abiertas

- **Cambio de modelo de movimiento:** de transform-por-índice a scroll/snap
  nativo → reescritura de CSS y migración de tests. Mitigado por la Fase 0.
- **Tamaño:** +~4 KB (core) solo en subpaths que lo importan; externalizado, no
  afecta a quien no use carousel/wheel.
- **¿Sobre-ingeniería?** Si el caso real es un slideshow simple full-width, el
  código actual ya cumple. Embla gana cuando hay drag con física, multi-slide,
  anchos variables, free-scroll o wheel vertical. Confirmar el caso de uso antes
  de migrar cada componente.
- **SSR/jsdom:** Embla mide layout; en tests hay que cubrir API/estado, no
  medición física (alineado con la política de tests del repo).

---

## 7. Definition of Done (Fase 1)

- [ ] `carousel` usa Embla internamente; API pública y eventos sin cambios.
- [ ] CSS migrado, selectores `data-*` intactos.
- [ ] Tests verdes (estado/eventos/ARIA), sin asserts de píxeles.
- [ ] Story y docs regeneradas (`pnpm docs:check` pasa).
- [ ] `embla-carousel` pineado, externalizado, declarado en `dependencies`.
- [ ] `pnpm test && pnpm lint && pnpm build` OK.
