# Flip

**When to use:** Flip a front/back pair of content 180°, like a physical playing card. Works completely standalone (it establishes its own `perspective`), or nested inside `Tilt` (with `perspective="none"`, since `Tilt` already establishes one) for a flippable, tilting card. Use it for a reveal-the-back interaction, a card in a hand of cards, or any two-sided toggle that should feel like a physical flip rather than a cross-fade.

**Keywords:** flip card, reveal back, card flip

**Import:** `import { Flip } from '@ahrowe/ui'`
**Types:** `import type { FlipProps, FlipHandle } from '@ahrowe/ui'`

```tsx
import { useState } from 'react';
import { Flip } from '@ahrowe/ui';

// Controlled, click to flip
const [flipped, setFlipped] = useState(false);

<Flip
  style={{ width: 220, height: 280 }}
  flipped={flipped}
  flipOnClick
  onFlip={setFlipped}
  front={<div>Click to flip</div>}
  back={<div>Here's the back!</div>}
/>
```

**Direction:** `flipAxis` picks which edge the flip hinges on (`'y'`, the default, hinges on the left/right edges; `'x'` hinges on the top/bottom edges). `reverse` flips the rotation sign on that same axis (+180deg vs -180deg), so the flip sweeps the opposite way without changing which edge it hinges on, useful for a hand of cards where neighboring cards should visually sweep toward each other rather than all sweeping the same direction:

```tsx
<Flip flipAxis="y" front={<Face />} back={<Back />} />              {/* sweeps left-right */}
<Flip flipAxis="y" reverse front={<Face />} back={<Back />} />      {/* sweeps right-left */}
<Flip flipAxis="x" front={<Face />} back={<Back />} />              {/* sweeps top-bottom */}
<Flip flipAxis="x" reverse front={<Face />} back={<Back />} />      {/* sweeps bottom-top */}
```

**Imperative control (custom animation, e.g. a shuffle):**

```tsx
import { useRef } from 'react';
import { Flip } from '@ahrowe/ui';
import type { FlipHandle } from '@ahrowe/ui';

const cardRef = useRef<FlipHandle>(null);

<Flip ref={cardRef} style={{ width: 220, height: 280 }} front={<Face />} back={<Back />} />

cardRef.current?.flip();               // toggles flip (uncontrolled instances only)
cardRef.current?.setFlipped(true);     // explicit set (uncontrolled instances only)
cardRef.current?.reset();              // back to unflipped (front-facing)
```

`flip()`/`setFlipped()` are no-ops (with a dev-only console warning) on a **controlled** instance (one with a `flipped` prop); update the `flipped` prop instead. `onFlip` fires only for a user-initiated flip (click or ref call), never for an externally-driven `flipped` prop change.

**Composing with `Tilt`:**

```tsx
import { Tilt, Flip } from '@ahrowe/ui';

<Tilt style={{ width: 220, height: 280 }}>
  <Flip style={{ width: '100%', height: '100%' }} perspective="none" front={<Face />} back={<Back />} />
</Tilt>
```

`Tilt` already establishes a `perspective` on its own wrapper, so pass `perspective="none"` to `Flip` when nesting it there to avoid two stacked perspective contexts. `Flip` doesn't inherit `Tilt`'s size on its own (it has no size of its own until you give it one), so size it to `100%` to fill `Tilt`'s surface.

**Sizing:** `Flip` doesn't impose a size of its own — give the root an explicit size via `style`/`className` (e.g. `style={{ width: 220, height: 280 }}`) whenever `front`/`back` are sized in percentages (`width: '100%'`) and need something to size against. Left unset, `Flip` sizes itself to fit its content instead of collapsing to 0×0, taking the larger of `front`/`back`'s natural size (both faces are stacked in the same CSS grid cell, so the wider/taller one sets the size for both, even though only the front is visible at rest).

**Motion:** the flip always animates over `flipDuration`, whether triggered by a click, a controlled prop change, or the ref API. Under `prefers-reduced-motion: reduce`, it flips instantly instead. `onFlipEnd` fires once the flip settles on its new face — after the transition completes normally, or synchronously under reduced motion, since no transition runs to wait for.

**Keyboard accessibility:** when `flipOnClick` is set, the root gets `role="button"`, `tabIndex={0}`, and `aria-pressed` reflecting the current flip state, and Enter/Space both toggle the flip the same way a click does, as long as the key was pressed on the root itself so a nested input or button keeps its own keys. None of this is applied when `flipOnClick` is `false`, since the element isn't interactive.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `front` | `ReactNode` | Front-face content (required) |
| `back` | `ReactNode` | Back-face content (required) |
| `flipped` | `boolean` | Controlled flip state |
| `defaultFlipped` | `boolean` | Initial flip state when uncontrolled (default `false`) |
| `flipOnClick` | `boolean` | Clicking toggles flip (uncontrolled) or calls `onFlip` (controlled) (default `false`) |
| `flipAxis` | `'x' \| 'y'` | Which edge the flip hinges on (default `'y'`) |
| `reverse` | `boolean` | Flips the rotation sign on `flipAxis`, so the sweep direction reverses (default `false`) |
| `flipDuration` | `number` | Flip transition duration in ms (default `600`) |
| `perspective` | `number \| 'none'` | CSS `perspective` in px on the perspective wrapper; pass `'none'` when nesting inside something that already establishes one, e.g. `Tilt` (default `800`) |
| `disabled` | `boolean` | Disables `flipOnClick`; programmatic flips via prop or ref still work (default `false`) |
| `onFlip` | `(flipped: boolean) => void` | Fires for a user-initiated flip (click or ref call), not fired for an externally-driven `flipped` prop change |
| `onFlipEnd` | `(flipped: boolean) => void` | Fires once the flip transition finishes settling on the new face, for any trigger. Fires synchronously under `prefers-reduced-motion: reduce` |

**`FlipHandle`** (via `ref`):

| Method | Description |
|--------|-------------|
| `flip()` | Toggles the flip state (uncontrolled instances only) |
| `setFlipped(value)` | Explicitly sets the flip state (uncontrolled instances only) |
| `reset()` | Returns to unflipped (front-facing) |

**Global defaults:** `Flip` adopts `ConfigProvider`, e.g. `defaultProps={{ Flip: { flipDuration: 400 } }}`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `root` `perspective` `surface` `front` `back`
