# Stamp

A rubber stamp that slams onto the page: it starts large and transparent and lands rotated with a small overshoot.

Docs: https://corsairui.vercel.app/docs/stamp

## Install

```bash
pnpm dlx shadcn@latest add @corsair-ui/stamp
```

Also adds: `utils`, `use-in-view`, `use-entrance`.
npm: `clsx`, `tailwind-merge`.

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rotate / from` | `number` | `-8 / 1.6` | Where it lands in degrees, and the scale it starts from. |
| `trigger / play` | `"load" \| "in-view" / boolean` | `"in-view"` | As in blur-text. |
| `delay / duration` | `number` | `0 / 320` | In ms. |

## Styling

Restyle it in the project that installs it: `className` on each part, the theme variables, and these attributes with arbitrary variants (`[&_[data-slot=…]]:…`, `data-[state=…]:…`).

- It is an inline-block with nothing but the rotation: give it the stamp look with classes (border-2, uppercase, tracking-widest, a colour).

data-slot: `stamp`

State attributes:

- `data-state` (computed)

## Accessibility

- The content stays readable to screen readers. With prefers-reduced-motion it holds still and shows the finished state.

## Examples

On the docs page: Sold out, Angles and weight. The usage examples in the source comments below are the reference.

## Source

What `shadcn add` writes into the project:

### stamp.tsx

```tsx
"use client";

import { useMemo, type ComponentProps, type CSSProperties, type Ref } from "react";

import { useEntrance, type EntranceTrigger } from "@/registry/default/hooks/use-entrance";
import { cn } from "@/registry/default/lib/utils";

function mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") ref(node);
      else if (ref) ref.current = node;
    }
  };
}

interface StampProps extends Omit<ComponentProps<"span">, "onAnimationEnd"> {
  as?: "span" | "div";
  /** Angle the stamp lands at, in degrees. */
  rotate?: number;
  /** Scale the stamp starts from before it slams down. */
  from?: number;
  /** "load" plays on first paint with CSS alone; "in-view" when the stamp scrolls into view. */
  trigger?: EntranceTrigger;
  /** In-view: play the first time only. */
  once?: boolean;
  /** Takes over from `trigger`: `false` holds the stamp up, `true` slams it down. */
  play?: boolean;
  /** Wait before the stamp falls, in ms. */
  delay?: number;
  /** How long the slam takes, in ms. */
  duration?: number;
  /** Called once the stamp has landed. */
  onAnimationComplete?: () => void;
}

/**
 * A rubber stamp that slams onto the page: it starts large and transparent,
 * then lands rotated with a small overshoot. The content stays readable for
 * screen readers, and the server HTML and `prefers-reduced-motion` show the
 * stamp already in place.
 *
 * @example
 * <Stamp className="border-primary text-primary rounded-md border-2 px-3 py-1 font-bold tracking-widest uppercase">Sold out</Stamp>
 */
function Stamp({
  as: Tag = "span",
  rotate = -8,
  from = 1.6,
  trigger = "in-view",
  once = true,
  play,
  delay = 0,
  duration = 320,
  onAnimationComplete,
  className,
  style,
  ref,
  ...props
}: StampProps) {
  const [observe, phase] = useEntrance<HTMLElement>({ trigger, once, play });
  const mergedRef = useMemo(() => mergeRefs(ref, observe), [ref, observe]);

  const stampStyle = {
    "--stamp-rotate": `${rotate}deg`,
    "--stamp-from": `${from}`,
    ...(phase === "static"
      ? {}
      : {
          animationDuration: `${duration}ms`,
          animationDelay: `${delay}ms`,
          animationPlayState: phase === "armed" ? "paused" : undefined,
        }),
    ...style,
  } as CSSProperties;

  return (
    <Tag
      ref={mergedRef as Ref<never>}
      data-slot="stamp"
      data-state={phase}
      className={cn(
        "inline-block [transform:rotate(var(--stamp-rotate))]",
        phase !== "static" && "motion-safe:animate-stamp",
        className
      )}
      style={stampStyle}
      onAnimationEnd={(event) => {
        if (event.target === event.currentTarget) onAnimationComplete?.();
      }}
      {...props}
    />
  );
}

export { Stamp, type StampProps };
```
