# Cursor
URL: https://remocn.dev/docs/ui/components/cursor

An animated cursor that follows a scripted path with click ripple and press effects

- Install: `npx shadcn@latest add @remocn/cursor`
- Vibe: clean
- Natural length: 120f @ 30fps

A motion atom that renders an animated cursor as a pure function of the `CursorStyle` it receives. The cursor reads no frame itself — the single time source is `useCursorPath` on the caller's side, which folds a path of waypoints into a resolved position, ripple, and press-dip visual. This is the **value-channel** variant of the primitive pattern: instead of a string-state timeline it drives numeric position channels directly.

## Installation

```bash
npx shadcn@latest add @remocn/cursor
```

> **Note**
>
> Installing `cursor` automatically installs the shared `remocn-ui` core (`lib/remocn-ui/`) via `registryDependencies` (`["remocn-ui"]`). You do not need to install it separately. Both `cursor` (the renderer) and `use-cursor-path` (the hook) are copied into your project.

## Waypoints

Instead of a string-state timeline, `useCursorPath` takes an array of `CursorWaypoint` objects that describe a scripted path:

```ts
interface CursorWaypoint {
  at: number;        // LOCAL (Sequence-relative) authored frame the cursor finishes arriving here
  x: number;        // Tip X in px, relative to the absolute wrapper's parent
  y: number;        // Tip Y in px, relative to the absolute wrapper's parent
  duration?: number; // Frames the move INTO this waypoint takes. Omitted → DEFAULT_DURATION (24)
  click?: boolean;   // Fire a click on arrival: ripple (~16 frames) + brief press dip
  press?: boolean;   // Hold the pressed look from this waypoint until the next one (drag)
  easing?: EasingName; // Override easing for the move into this waypoint. Default "inOut"
}
```

`at` is the **arrival frame** — the frame when the cursor finishes its move. The move itself runs over `[at - duration, at)`. Before the first waypoint arrives, the cursor parks at the first waypoint's position.

### Numeric-channel deviation

`Cursor` differs from state atoms (Button, Input, etc.) in one key way: there is no string `state` union and no `useCurrentState` snap path. The cursor's visual is always numeric — a position plus ripple and press scalars — so `useCursorPath` folds the waypoint path directly into a `CursorStyle` value, analogous to how `useButtonTransition` returns a `ButtonStyle`. The snap equivalent is simply parking the cursor at a fixed position without calling the hook.

## Snap usage

Park the cursor at a fixed position without the hook:

```tsx
import { Cursor } from "@/components/remocn/cursor";

export const Scene = () => (
  <div style={{ position: "relative", width: "100%", height: "100%" }}>
    <Cursor
      style={{ x: 200, y: 150, scale: 1, rippleOpacity: 0, rippleScale: 0 }}
      variant="pointer"
    />
  </div>
);
```

## Smooth path usage

Drive the cursor along a scripted path with `useCursorPath`:

```tsx
import { Cursor } from "@/components/remocn/cursor";
import { useCursorPath } from "@/components/remocn/use-cursor-path";

export const Scene = () => {
  const style = useCursorPath([
    { at: 0,  x: 80,  y: 60  },
    { at: 40, x: 280, y: 160, duration: 28 },
    { at: 72, x: 280, y: 160, click: true, duration: 0 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Cursor style={style} variant="pointer" />
    </div>
  );
};
```

Each `at` is a **Sequence-local authored frame**. The cursor eases from each waypoint's position to the next. A `click: true` waypoint fires a 16-frame ripple and a brief press dip starting at its `at` frame.

### Frame-syncing with other components

The power of the waypoint model is composing the cursor timeline with the state timelines of other components. When a `click` waypoint fires at frame 72, schedule a Button's `press` step to start a few frames earlier (around 68) so the button begins pressing just before the click lands:

```tsx
import { Cursor } from "@/components/remocn/cursor";
import { useCursorPath } from "@/components/remocn/use-cursor-path";
import { Button } from "@/components/remocn/button";
import { useButtonTransition } from "@/components/remocn/use-button-transition";

export const Scene = () => {
  const cursorStyle = useCursorPath([
    { at: 0,  x: 80,  y: 60  },
    { at: 40, x: 280, y: 160, duration: 28 },
    { at: 72, x: 280, y: 160, click: true, duration: 0 },
  ]);

  const buttonStyle = useButtonTransition([
    { at: 40, state: "hover",   duration: 16 },
    { at: 68, state: "press",   duration: 8  },
    { at: 76, state: "loading", duration: 6  },
    { at: 108, state: "success", duration: 16 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <div style={{ position: "absolute", left: "50%", top: "50%", transform: "translate(-50%, -50%)" }}>
        <Button label="Continue" style={buttonStyle} />
      </div>
      <Cursor style={cursorStyle} variant="pointer" />
    </div>
  );
};
```

### Drag / held press

Set `press: true` on a waypoint to hold the pressed look from that waypoint's arrival until the next waypoint arrives. Useful for simulating a drag gesture:

```tsx
const style = useCursorPath([
  { at: 0,  x: 100, y: 100 },
  { at: 30, x: 200, y: 100, press: true },  // hold press from here…
  { at: 60, x: 300, y: 100 },               // …until cursor reaches here
]);
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `style` | `CursorStyle` | — | Resolved animated visual. Feed it an interpolated CursorStyle from useCursorPath. When omitted the cursor parks at origin with no ripple. |
| `variant` | `"arrow" \| "pointer"` | `"arrow"` | macOS arrow cursor or hand pointer. Static choice — not animated between waypoints. |
| `size` | `number` | `28` | Rendered cursor height in px. The SVG scales uniformly to this height. |
| `rippleColor` | `string` | — | Color of the click ripple ring. Defaults to theme.primary. |
| `theme` | `Partial<RemocnTheme>` | — | Per-component theme override. Must be concrete oklch/hex/rgb values — not CSS custom properties. |
| `mode` | `"light" \| "dark"` | `"light"` | Sets the base palette. Overrides the mode on RemocnUIProvider when both are present. |
| `className` | `string` | — | Optional className applied to the absolute wrapper div. |

## Use when
- Guiding the viewer's attention through a UI by animating a pointer between elements.
- Showing click, hover, and drag interactions in a product demo without real interactivity.
- Composing inside `checkout-flow` or a custom orchestrator where the cursor drives the narrative.

## Don't use when
- You need a blinking text insertion cursor — use `caret` instead.
- There is no interaction to demonstrate and the cursor would be visual noise.
- You need a crosshair or custom SVG cursor — the variant set is `arrow` / `pointer`; extend or render custom SVG directly.