# Your own UI on core

If your product has its own way of moving, keep `core` and leave `ui` out. The rules stay the same (the queue, the presses, when a card counts as opened) and the picture is whatever you draw. This guide builds a working reveal from a `div` and CSS transitions.

> Live preview: see it play at https://bettertyped.github.io/react-cards-opening/docs/guides/own-ui-on-core/
>
> No ui package and no GSAP in this one. Press the first button, then the card.

## What a renderer owes the rules

1. **Say where the card is.** Call `opening.enter(phase)` as the reveal moves on: `hum`, `turn`, `hold`, `land`.
2. **Say when it is face up.** Call `opening.reveal()`. That is when `onOpen` fires. It is safe to call twice.
3. **Say when it is home.** Call `opening.landed()`. The next card comes up, or the run ends.
4. **Be ready to jump.** Hand `opening.attach()` four functions: turn now, finish now, cancel, close.

## The effect

```tsx
import { holdSeconds, showFor, tierOf, DEFAULT_RARITIES, useCardOpening, useOpeningPhase } from "@react-cards-opening/core";

function Reveal({ cards, opened, onOpen }) {
  const hunt = useCardOpening({ cards, opened, onOpen });
  const { opening, run, step, current } = hunt;
  const phase = useOpeningPhase(opening);

  useEffect(() => {
    if (!run || !current) return;
    const plan = showFor(tierOf(DEFAULT_RARITIES, current.rarity).effect);
    let timers: number[] = [];
    const after = (ms: number, act: () => void) => timers.push(window.setTimeout(act, ms));
    const clear = () => timers.splice(0).forEach(clearTimeout);

    const land = () => {
      clear();
      opening.reveal();
      opening.enter("land");
      after(320, opening.landed);
    };
    const turn = () => {
      clear();
      opening.enter("turn");
      after(420, () => {
        opening.reveal();
        opening.enter("hold");
      });
      after(420 + holdSeconds([current.name, current.line], plan) * 1000, land);
    };

    opening.enter("hum");
    after(900, turn);
    const detach = opening.attach({ turn, finish: land, cancel: clear, close: () => {} });
    return () => {
      clear();
      detach();
    };
  }, [run?.id, step]);

  return (
    <button className="card" data-phase={phase} onClick={hunt.pressCard} disabled={!run}>
      {current?.name}
    </button>
  );
}
```

## The look

```css
.card {
  transition: transform 0.42s cubic-bezier(0.34, 1.4, 0.64, 1), opacity 0.3s;
  transform: rotateY(180deg);
}
.card[data-phase="hum"] { animation: shake 0.3s 2; }
.card[data-phase="turn"],
.card[data-phase="hold"] { transform: rotateY(0); }
.card[data-phase="land"] { transform: scale(0.6); opacity: 0; }
```

## What you get for free

- `hunt.pressCard` turns a face-down card and sends a face-up one home. You did not write that branch.
- `hunt.pressBackdrop` stops the run the right way: a turned card still lands, a face-down one goes back unopened.
- `hunt.openAll` opens everything and calls your `cancel`.
- A replay never calls `onOpen`.
- The [show plan](https://bettertyped.github.io/react-cards-opening/docs/core/show-plan/) tells you how loud to be for the tier, and `holdSeconds` how long to wait.

## With an animation library

The same contract works with any clock. With a timeline, `turn` is a seek to a label and `finish` is a jump to the end. That is exactly what [CardOpening](https://bettertyped.github.io/react-cards-opening/docs/components/card-opening/) does with GSAP, and reading its source is the fastest way to a renderer of your own.

## Related

- [useCardOpening](https://bettertyped.github.io/react-cards-opening/docs/core/use-card-opening/)
- [Show plan](https://bettertyped.github.io/react-cards-opening/docs/core/show-plan/)
- [Core](https://bettertyped.github.io/react-cards-opening/docs/core/)
- [Reduced motion](https://bettertyped.github.io/react-cards-opening/docs/core/reduced-motion/)

Source: https://bettertyped.github.io/react-cards-opening/docs/guides/own-ui-on-core/
