# Carrusel

> La tira que se arrastra, con coverflow atado al scroll y sin una línea de JavaScript. No se importa solo.

Paquete: `@pantherkit/design-system` · Archivo: `src/components/carrusel.css`

Se importa con:

```css
@import "@pantherkit/design-system/tokens.css";  /* siempre antes */
@import "@pantherkit/design-system/carrusel.css";
```

La pieza se entrega como CSS, no como componente React: se aplica poniendo
las clases en tu propio marcado. No hay import de JavaScript que hacer.

## Qué es

**Esta es la única pieza que no entra con `components.css`, y es deliberado.** Sus reglas son globales —`.car`, `.car-celda`— y se aplicaban encima del carrusel que la landing ya tenía: dos coverflow superpuestos, y las tarjetas de los lados salían lavadas. Quien lo quiera lo importa explícitamente. Es la diferencia entre publicar una pieza y activarla en todos los que ya tienen la suya.

**Funciona sin JavaScript.** El encaje lo hace el navegador con `scroll-snap`: nadie escribe la física, nadie mide posiciones por fotograma. El coverflow va atado a `view-timeline`, así que el 3D sigue al dedo y al trackpad en tiempo real. Lo único que necesita guion son los dos botones.

## Clases

| Clase | Qué hace |
| --- | --- |
| `.car` | La tira. Scroll nativo con `scroll-snap-type: x mandatory` y `tabindex="0"`, así que recibe foco de teclado — y por eso tiene `:focus-visible` azul. El foco es azul, nunca naranja: aquí el naranja significa punto de decisión y el azul interactividad. |
| `.car-celda` | La celda fija el ancho y encaja. El hijo de dentro es el único que se transforma. **No metas ninguna `transform` aquí.** |
| `.car-mandos / .car-boton` | Los dos botones. Van fuera de la tira, arriba junto al titular. La flecha se adelanta un poco en la dirección que va a mover la tira: el botón dice hacia dónde antes de pulsarlo. |
| `[data-coverflow="no"]` | Apaga el 3D y deja la tira plana. |

## Mandos

Custom properties que se redefinen desde fuera —con `style` o con una clase
propia— sin tocar el archivo del sistema.

| Propiedad | Por defecto | Qué controla |
| --- | --- | --- |
| `--car-sangria` | `1rem / 1.5rem / 2rem` | Cuánto se sale la tira del margen del contenedor. Es una variable local para que el margen negativo, el relleno y el `scroll-padding` no puedan desincronizarse: los tres leen el mismo sitio. |
| `--car-ancho` | `min(20rem, 82vw) / 21rem / 24rem` | El ancho de una celda. En móvil llena casi la pantalla pero deja asomar la siguiente, que es lo que dice que la tira se arrastra. |

## El marcado

Esto es exactamente lo que se pinta en la página `/componentes/carrusel`:
el mismo texto se inyecta ahí como pieza viva y se imprime aquí. No pueden
separarse.

### La tira con coverflow

Los mandos van **arriba, junto al titular**, y no flotando sobre las tarjetas: encima de una captura tapan justo lo que se quiere ver, y ahí no se sabe si el clic abre el proyecto o mueve el carrusel. Arrástrala con el trackpad y mira cómo giran las celdas de los bordes: eso es `view-timeline`, no un guion.

*Nota: Los botones necesitan guion —encender, apagar y desplazar—; el resto es CSS. El de la vitrina está debajo.*

```html
<div class="mb-4 flex items-end justify-between gap-4">
  <h3 class="text-xl font-semibold tracking-[-0.012em]">Proyectos</h3>
  <div class="car-mandos" data-car-mandos="tira-demo">
    <button type="button" class="car-boton" data-car-prev aria-label="Anterior" disabled>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>
    </button>
    <button type="button" class="car-boton" data-car-next aria-label="Siguiente">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
    </button>
  </div>
</div>

<div class="car" id="tira-demo" role="region" aria-label="Proyectos" tabindex="0">
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Plataforma</p>
      <h4 class="mt-2 text-base font-semibold">Uno</h4>
      <p class="mt-2 text-sm text-text-secondary">La celda fija el ancho; la tarjeta de dentro es la que gira.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Infraestructura</p>
      <h4 class="mt-2 text-base font-semibold">Dos</h4>
      <p class="mt-2 text-sm text-text-secondary">El giro es hacia dentro, como se apila una baraja. Al revés parece que se caen.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Producto</p>
      <h4 class="mt-2 text-base font-semibold">Tres</h4>
      <p class="mt-2 text-sm text-text-secondary">Solo la celda que pasa por el centro está de frente: la meseta es un punto, no un tramo.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Datos</p>
      <h4 class="mt-2 text-base font-semibold">Cuatro</h4>
      <p class="mt-2 text-sm text-text-secondary">Sin JavaScript la tira se arrastra igual. Lo que faltan son los dos botones.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Interno</p>
      <h4 class="mt-2 text-base font-semibold">Cinco</h4>
      <p class="mt-2 text-sm text-text-secondary">Al llegar al final el botón no desaparece: se apaga.</p>
    </article>
  </div>
</div>
```

### Plana, sin coverflow

El 3D es la capa de arriba, nunca el camino principal. `data-coverflow="no"` lo apaga: queda la tira que se arrastra y encaja, que ya funcionaba entera. Es también lo que se ve donde `animation-timeline` no existe —Firefox hoy—, así que conviene mirarlo.

```html
<div class="car" data-coverflow="no" style="--car-ancho: 15rem" role="region" aria-label="Sin coverflow" tabindex="0">
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Plana</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Sin giro</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Sigue encajando</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Y sigue siendo scroll nativo</article></div>
</div>
```

## Cuándo NO usarla

La parte que más valor tiene. Cada una está aquí porque costó algo.

### Importándolo desde `components.css`.

No está ahí a propósito. Sus selectores son globales y pisan el carrusel que el consumidor ya tenga: en la landing se superpusieron dos coverflow y las tarjetas de los lados salían lavadas. Se importa explícitamente, y solo quien lo quiere.

### Poniendo una `transform` en `.car-celda`.

Ni `scale`, ni `translate`, ni un `translateZ(0)` «para acelerar». El navegador calcula los puntos de encaje sobre la caja **ya transformada**: con el coverflow escalando, el destino se movería con el propio scroll —las flechas apuntan a 400 y aterrizan en 403, cada vez en un sitio distinto— y la tira da tirones. Por eso hay dos cajas y no una. Esto costó horas.

### Con los mandos flotando sobre las tarjetas.

Encima de una captura tapan justo lo que se quiere ver, y ahí no se sabe si el clic abre el proyecto o mueve el carrusel.

### Para contenido que hay que comparar.

Un carrusel esconde: en cualquier momento hay algo fuera de pantalla. Si hay que ver todo a la vez para decidir, es una rejilla. La tira sirve para invitar a explorar, no para elegir.

### Escondiendo el mando al llegar al extremo.

Se apaga con `:disabled`, no desaparece. Un mando que se va mueve el layout, deja al otro bailando de sitio y encima le roba el foco al teclado a media navegación.

### Con un hover que levante la tarjeta.

Su `transform` pelearía con la del scroll: gana la última que se compone y la tarjeta pega un salto. Dentro de la tira el hover se dice con el borde y la sombra.

## Tokens de los que depende

Si `tokens.css` no está importado antes, estas variables no existen y la
pieza se pinta con los valores iniciales del navegador —que no es un fallo
visible, es una pieza silenciosamente rota.

- `--color-border`
- `--color-border-hover`
- `--color-link`
- `--color-text-primary`
- `--color-bg-surface-1`
- `--radius-xl`
- `--radius-full`
- `--dur-fast`
- `--dur-base`
- `--dur-slow`
- `--ease-spring`
- `--ease-out-expo`

## Qué queda cuando algo falla

- Sin JavaScript la tira se arrastra igual con dedo, trackpad o teclado: tiene `tabindex` y scroll nativo. Se pierden los dos botones, no el contenido.
- Sin `animation-timeline` —Firefox hoy— queda el carrusel plano, que ya funcionaba entero. El `@supports` va delante a propósito y ese orden no se invierte.
- Por debajo de 640px no hay coverflow: se ve una celda, y girarla sería girar la única cosa que hay que leer.
- Con `prefers-reduced-motion` el coverflow no se activa, el scroll deja de animarse y los mandos ya no escalan. El hover y el foco siguen cambiando borde y fondo.

## Accesibilidad

- `role="region"` + `aria-label` + `tabindex="0"`: la tira es un contenedor con scroll y tiene que poder recorrerse con teclado.
- `:focus-visible` con `outline: 2px solid var(--color-link)`. Azul, porque el naranja está reservado a punto de decisión.
- Los mandos miden 2.75rem = 44px. El icono mide 1.05rem; el resto es blanco que existe para poder acertarle con el pulgar.
- Los botones son iconos sin texto: `aria-label` obligatorio.

## Lo que el sistema todavía no resuelve

Valores literales y huecos conocidos. Se escriben para que nadie los
confunda con una decisión.

- La opacidad `0.25` del mando apagado es literal: no hay escala de opacidades para estados inertes.
- El `perspective: 1600px` también. No hay tokens de profundidad.

---

Versión viva: <https://design.wearefaber.com/componentes/carrusel>
Índice de componentes: </componentes.md>
