---
title: "Tools, cursors & touch — React"
description: "The active tool decides what the pointer does: select, pan, draw. Switch tools, add your own, and choose cursors, touch behavior and haptics."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/ui/tools"
---

# Tools, cursors & touch

At any moment one **tool** is active, and it decides what a click or a drag on the page does: select
text, scroll, draw a rectangle, place a stamp. The interaction plugin keeps track of it, and every
feature that uses the pointer works through it. Register `interactionPlugin()`:

**`interaction-plugin.tsx`**

```tsx
import { interactionPlugin } from '@embedpdf/react/interaction';
import { renderPlugin } from '@embedpdf/react/render';
import { stagePlugin } from '@embedpdf/react/stage';

export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];
```

Then switch tools from your toolbar:

**`tool-switch.tsx`**

```tsx
import { useInteraction, useInteractionState } from '@embedpdf/react/interaction';

export function ToolSwitch() {
  const interaction = useInteraction();
  const { activeToolId } = useInteractionState();

  return (
    <>
      <button aria-pressed={activeToolId === 'pointer'} onClick={() => interaction.activateTool('pointer')}>
        Select
      </button>
      <button aria-pressed={activeToolId === 'pan'} onClick={() => interaction.activateTool('pan')}>
        Hand
      </button>
    </>
  );
}
```

**`switch.tsx`**

```tsx
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
  interactionPlugin,
  useInteraction,
  useInteractionState,
} from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './switch.css';

const engine = localEngine();
// The document opens with the hand tool.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin({ defaultTool: 'pan' }),
  selectionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const LABELS: Record<string, string> = { pointer: 'Select', pan: 'Hand' };
const HINTS: Record<string, string> = {
  pointer: 'A drag selects text',
  pan: 'A drag scrolls the pages',
};

// A button for every tool you can switch to, the active one pressed.
function ToolSwitch() {
  const interaction = useInteraction();
  const { activeToolId, tools } = useInteractionState();

  return (
    <div className="toolbar">
      <div className="segmented" role="group" aria-label="Tool">
        {tools.map((tool) => (
          <button
            key={tool.id}
            type="button"
            className="segment"
            aria-pressed={tool.id === activeToolId}
            onClick={() => interaction.activateTool(tool.id)}
          >
            {LABELS[tool.id] ?? tool.id}
          </button>
        ))}
      </div>
      <output className="readout">{activeToolId ? HINTS[activeToolId] : ''}</output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <ToolSwitch />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`switch.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tool switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment {
  min-width: 72px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.segment:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## The built-in tools

| Tool      | A drag on the page                             | Also                                     |
| :-------- | :--------------------------------------------- | :--------------------------------------- |
| `pointer` | Selects text, or selects and moves annotations | Fills forms, follows links. The default. |
| `pan`     | Scrolls, like Acrobat's hand tool              | Fills forms, follows links               |

Plugins add their own: the annotation plugin a tool per kind (`square`, `highlight`, `note`…), the form
plugin a tool per field, redaction `redact`, measurements `distance`. They're all switched the same
way, with `activateTool(id)`, and `tools` in [State](#state) lists them.

## Hold a tool for a moment

`pushTool()` switches to a tool and remembers the one before; `popTool()` goes back. That's how
"hold Space to pan" works:

**`hold-space.tsx`**

```tsx
import { useEffect } from 'react';
import { useInteraction } from '@embedpdf/react/interaction';

export function HoldSpaceToPan() {
  const interaction = useInteraction();

  useEffect(() => {
    const down = (event: KeyboardEvent) => {
      if (event.code === 'Space' && !event.repeat) interaction.pushTool('pan');
    };
    const up = (event: KeyboardEvent) => {
      if (event.code === 'Space') interaction.popTool();
    };
    window.addEventListener('keydown', down);
    window.addEventListener('keyup', up);
    return () => {
      window.removeEventListener('keydown', down);
      window.removeEventListener('keyup', up);
    };
  }, [interaction]);

  return null;
}
```

**`hold-space.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
  interactionPlugin,
  useInteraction,
  useInteractionState,
} from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './hold-space.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// While Space is held over the pages, the hand tool is active; on release, the tool from before.
function HoldSpaceToPan({ children }: { children: ReactNode }) {
  const interaction = useInteraction();
  // Only over the pages, so Space still scrolls the rest of your page.
  const viewer = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let held = false;
    const down = (event: KeyboardEvent) => {
      if (event.code !== 'Space' || !viewer.current?.matches(':hover')) return;
      event.preventDefault();
      if (event.repeat || held) return;
      held = true;
      interaction.pushTool('pan');
    };
    const up = (event: KeyboardEvent) => {
      if (event.code !== 'Space' || !held) return;
      held = false;
      interaction.popTool();
    };
    window.addEventListener('keydown', down);
    window.addEventListener('keyup', up);
    return () => {
      window.removeEventListener('keydown', down);
      window.removeEventListener('keyup', up);
    };
  }, [interaction]);

  return (
    <div ref={viewer} className="viewer">
      {children}
    </div>
  );
}

function ActiveTool() {
  const { activeToolId } = useInteractionState();
  const panning = activeToolId === 'pan';

  return (
    <div className="toolbar">
      <span className="badge" data-active={panning}>
        {panning ? 'Hand' : 'Select'}
      </span>
      <span className="readout">
        {panning ? 'Drag to scroll, then let go of Space' : 'Hold Space over the pages to scroll'}
      </span>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <ActiveTool />
        <HoldSpaceToPan>
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <SelectionLayer />
              </>
            )}
          </Stage>
        </HoldSpaceToPan>
      </DocumentGate>
    </Viewer>
  );
}
```

**`hold-space.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The active tool, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.badge {
  min-width: 56px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 12px;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.badge[data-active='true'] {
  color: var(--accent);
  background: var(--accent-tint);
  border-color: transparent;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

`activateDefaultTool()` goes back to the default tool, `pointer` unless you set another.

## Your own tool

A tool of your own gets the pointer on the page, in page coordinates:

```ts
const remove = interaction.registerTool({
  id: 'pin',
  cursor: 'copy',
  onPointerDown: ({ page, point }) => {
    addPin(page, point);
    return true; // this tool handled the click
  },
});

interaction.activateTool('pin');
```

**`pin.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, pageRefsEqual } from '@embedpdf/react/runtime';
import type { OpenInput, PageContextValue, PageRef } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
  interactionPlugin,
  svgCursor,
  useInteraction,
  useInteractionState,
} from '@embedpdf/react/interaction';
import { localEngine } from '@embedpdf/engine';

import './pin.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

interface Pin {
  readonly id: number;
  readonly page: PageRef;
  readonly point: { readonly x: number; readonly y: number };
}

// The cursor is the pin itself; its tip is the point that clicks.
const PIN_CURSOR = svgCursor({
  svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M12 23s7.5-7.4 7.5-12.5a7.5 7.5 0 0 0-15 0C4.5 15.6 12 23 12 23z" fill="#e5484d" stroke="#fff" stroke-width="1.5"/>
    <circle cx="12" cy="10.5" r="2.8" fill="#fff"/>
  </svg>`,
  hotspot: { x: 12, y: 23 },
  fallback: 'copy',
});

// The pins on one page, placed in its pixels at the last moment.
function PinLayer({ page, pins }: { page: PageContextValue; pins: readonly Pin[] }) {
  return (
    <>
      {pins
        .filter((pin) => pageRefsEqual(pin.page, page.ref))
        .map((pin) => {
          const { x, y } = page.transform.toPixels(pin.point);
          return (
            <span key={pin.id} className="pin" style={{ left: x, top: y }}>
              {pin.id}
            </span>
          );
        })}
    </>
  );
}

function PinBoard() {
  const interaction = useInteraction();
  const { activeToolId } = useInteractionState();
  const [pins, setPins] = useState<readonly Pin[]>([]);

  // A tool of your own: a click on a page drops a pin there, in page coordinates.
  useEffect(() => {
    const remove = interaction.registerTool({
      id: 'pin',
      cursor: PIN_CURSOR,
      onPointerDown: ({ page, point }) => {
        setPins((current) => [...current, { id: current.length + 1, page, point }]);
        return true; // this tool handled the click
      },
    });
    interaction.activateTool('pin');
    return remove;
  }, [interaction]);

  return (
    <>
      <div className="toolbar">
        <div className="segmented" role="group" aria-label="Tool">
          <button
            type="button"
            className="segment"
            aria-pressed={activeToolId === 'pin'}
            onClick={() => interaction.activateTool('pin')}
          >
            Pin
          </button>
          <button
            type="button"
            className="segment"
            aria-pressed={activeToolId === 'pan'}
            onClick={() => interaction.activateTool('pan')}
          >
            Hand
          </button>
        </div>
        <button
          type="button"
          className="button"
          disabled={pins.length === 0}
          onClick={() => setPins([])}
        >
          Clear
        </button>
        <output className="readout">
          {pins.length === 0 ? 'Click a page to drop a pin' : `${pins.length} pins`}
        </output>
      </div>
      <Stage className="stage">
        {(page) => (
          <>
            <RenderLayer />
            <PinLayer page={page} pins={pins} />
          </>
        )}
      </Stage>
    </>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <PinBoard />
      </DocumentGate>
    </Viewer>
  );
}
```

**`pin.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --pin: #e5484d;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tools, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment,
.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.segment {
  min-width: 64px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.button {
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 10px 13px;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.segment:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A pin: its tip on the point that was clicked */
.pin {
  position: absolute;
  box-sizing: border-box;
  min-width: 24px;
  height: 24px;
  padding: 0 5px;
  transform: translate(-50%, calc(-100% - 7px));
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  color: var(--surface);
  background: var(--pin);
  border: 2px solid var(--surface);
  border-radius: 999px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

.pin::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 100%;
  border: 6px solid transparent;
  border-top-color: var(--pin);
  transform: translate(-50%, 1px);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

| Option                         | What it is                                                                                |
| :----------------------------- | :---------------------------------------------------------------------------------------- |
| `id`                           | The tool's name, for `activateTool(id)`                                                   |
| `cursor`                       | The cursor over the page, any CSS cursor                                                  |
| `onPointerDown`                | A press on a page: `page`, `point`, `modifiers`. Return `true` to take the gesture        |
| `onPointerMove`, `onPointerUp` | The rest of a gesture you took                                                            |
| `onHover`                      | The pointer moving over a page, with no button pressed                                    |
| `touch`                        | `'draw'`: one finger drives the tool; `'tap'` (default): a tap is a click, a drag scrolls |

`registerTool()` returns a function that removes the tool again. A tool you drag with takes the press and
follows the gesture:

**`ruler.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, pageRefsEqual } from '@embedpdf/react/runtime';
import type { OpenInput, PageContextValue, PageRef } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useInteraction } from '@embedpdf/react/interaction';
import { localEngine } from '@embedpdf/engine';

import './ruler.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

interface Point {
  readonly x: number;
  readonly y: number;
}

interface Measurement {
  readonly page: PageRef;
  readonly from: Point;
  readonly to: Point;
}

// Page coordinates are points, 72 to the inch.
const lengthOf = ({ from, to }: Measurement) => {
  const inches = Math.hypot(to.x - from.x, to.y - from.y) / 72;
  return `${inches.toFixed(2)} in · ${(inches * 2.54).toFixed(1)} cm`;
};

// The measured line on its page, placed in the page's pixels.
function RulerLayer({
  page,
  measurement,
}: {
  page: PageContextValue;
  measurement: Measurement | null;
}) {
  if (!measurement || !pageRefsEqual(measurement.page, page.ref)) return null;
  const from = page.transform.toPixels(measurement.from);
  const to = page.transform.toPixels(measurement.to);

  return (
    <>
      <svg className="ruler-layer">
        <line className="ruler-line" x1={from.x} y1={from.y} x2={to.x} y2={to.y} />
        <circle className="ruler-end" cx={from.x} cy={from.y} r={4} />
        <circle className="ruler-end" cx={to.x} cy={to.y} r={4} />
      </svg>
      <span className="ruler-label" style={{ left: (from.x + to.x) / 2, top: (from.y + to.y) / 2 }}>
        {lengthOf(measurement)}
      </span>
    </>
  );
}

function Ruler() {
  const interaction = useInteraction();
  const [measurement, setMeasurement] = useState<Measurement | null>(null);
  const [pointer, setPointer] = useState<Point | null>(null);

  // A tool you drag with: the press starts a line, the moves stretch it, the release ends it.
  useEffect(() => {
    const remove = interaction.registerTool({
      id: 'ruler',
      cursor: 'crosshair',
      touch: 'draw', // one finger measures, two fingers scroll and zoom
      onPointerDown: ({ page, point }) => {
        setMeasurement({ page, from: point, to: point });
        return true;
      },
      onPointerMove: ({ point }) => {
        setMeasurement((current) => current && { ...current, to: point });
      },
      onPointerUp: ({ point }) => {
        setMeasurement((current) => current && { ...current, to: point });
      },
      onHover: ({ point }) => setPointer(point),
    });
    interaction.activateTool('ruler');
    return remove;
  }, [interaction]);

  return (
    <>
      <div className="toolbar">
        <output className="readout">
          {measurement ? lengthOf(measurement) : 'Drag on a page to measure'}
        </output>
        <output className="readout muted">
          {pointer ? `x ${Math.round(pointer.x)} · y ${Math.round(pointer.y)} pt` : ''}
        </output>
      </div>
      <Stage className="stage">
        {(page) => (
          <>
            <RenderLayer />
            <RulerLayer page={page} measurement={measurement} />
          </>
        )}
      </Stage>
    </>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Ruler />
      </DocumentGate>
    </Viewer>
  );
}
```

**`ruler.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --ruler: #e5484d;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The measurement, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
  min-height: 20px;
  margin-bottom: 14px;
}

.readout {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.readout.muted {
  font-size: 13px;
  font-weight: 450;
  color: var(--muted);
}

/* The line on the page, and its length at its middle */
.ruler-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.ruler-line {
  stroke: var(--ruler);
  stroke-width: 2;
  stroke-dasharray: 6 4;
}

.ruler-end {
  fill: var(--surface);
  stroke: var(--ruler);
  stroke-width: 2;
}

.ruler-label {
  position: absolute;
  transform: translate(-50%, calc(-100% - 8px));
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--surface);
  background: var(--ruler);
  border-radius: 999px;
  padding: 4px 9px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.25);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Cursors

Each tool has a cursor. Over things the tool can act on, the viewer switches to the right one by itself:
a move or resize cursor over an annotation, a text cursor over text, a hand over a link. You choose the
tool's own cursor, in three steps:

- **A CSS cursor**: `cursor: 'crosshair'`.
- **An image**: `cursor: svgCursor({ svg: penIcon, hotspot: { x: 2, y: 22 } })`. The SVG needs a
  `width` and a `height`, and `hotspot` is the point that clicks.
- **An image that changes**, such as a pen in the current color: `useToolCursor()` from a component, which follows the values it reads.

**`pen-cursor.tsx`**

```tsx
import { useToolCursor } from '@embedpdf/react/interaction';
import { penIcon } from './icons';

export function PenCursor({ color }: { color: string }) {
  useToolCursor({ toolId: 'ink', cursors: { crosshair: { svg: penIcon(color), hotspot: { x: 2, y: 22 } } } });
  return null;
}
```

**`cursor.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, pageRefsEqual } from '@embedpdf/react/runtime';
import type { OpenInput, PageContextValue, PageRef } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useInteraction, useToolCursor } from '@embedpdf/react/interaction';
import { localEngine } from '@embedpdf/engine';

import './cursor.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const COLORS = ['#e5484d', '#2f80ed', '#30a46c', '#1a2748'];

// A pen in the current color, with its tip at the bottom left.
const penIcon = (color: string) =>
  `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M3 21l1.6-5.6L16.2 3.8a2.2 2.2 0 0 1 3.1 3.1L7.7 18.5z" fill="${color}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>
  </svg>`;

interface Dot {
  readonly id: number;
  readonly page: PageRef;
  readonly point: { readonly x: number; readonly y: number };
  readonly color: string;
}

// While this is mounted, the dot tool's crosshair is a pen in `color`.
function PenCursor({ color }: { color: string }) {
  useToolCursor({
    toolId: 'dot',
    cursors: { crosshair: { svg: penIcon(color), hotspot: { x: 3, y: 21 } } },
  });
  return null;
}

function DotLayer({ page, dots }: { page: PageContextValue; dots: readonly Dot[] }) {
  return (
    <>
      {dots
        .filter((dot) => pageRefsEqual(dot.page, page.ref))
        .map((dot) => {
          const { x, y } = page.transform.toPixels(dot.point);
          return (
            <span key={dot.id} className="dot" style={{ left: x, top: y, background: dot.color }} />
          );
        })}
    </>
  );
}

function Dots() {
  const interaction = useInteraction();
  const [color, setColor] = useState(COLORS[0]);
  const [penCursor, setPenCursor] = useState(true);
  const [dots, setDots] = useState<readonly Dot[]>([]);
  // The tool reads the color at the moment of the click.
  const colorRef = useRef(color);
  colorRef.current = color;

  useEffect(() => {
    const remove = interaction.registerTool({
      id: 'dot',
      cursor: 'crosshair',
      onPointerDown: ({ page, point }) => {
        setDots((current) => [
          ...current,
          { id: current.length + 1, page, point, color: colorRef.current },
        ]);
        return true;
      },
    });
    interaction.activateTool('dot');
    return remove;
  }, [interaction]);

  return (
    <>
      {penCursor && <PenCursor color={color} />}
      <div className="toolbar">
        <div className="swatches" role="radiogroup" aria-label="Color">
          {COLORS.map((swatch) => (
            <button
              key={swatch}
              type="button"
              role="radio"
              aria-checked={swatch === color}
              aria-label={swatch}
              className="swatch"
              style={{ background: swatch }}
              onClick={() => setColor(swatch)}
            />
          ))}
        </div>
        <label className="check">
          <input
            type="checkbox"
            checked={penCursor}
            onChange={(event) => setPenCursor(event.target.checked)}
          />
          Pen cursor
        </label>
      </div>
      <Stage className="stage">
        {(page) => (
          <>
            <RenderLayer />
            <DotLayer page={page} dots={dots} />
          </>
        )}
      </Stage>
    </>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Dots />
      </DocumentGate>
    </Viewer>
  );
}
```

**`cursor.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The colors and the cursor switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin-bottom: 14px;
}

.swatches {
  display: inline-flex;
  gap: 8px;
}

.swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}

.swatch[aria-checked='true'] {
  box-shadow: 0 0 0 2px var(--ink);
}

.swatch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink);
  cursor: pointer;
}

/* A dot where the pen clicked */
.dot {
  position: absolute;
  width: 12px;
  height: 12px;
  transform: translate(-50%, -50%);
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 1px 4px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

`cursors` replaces the tool's cursors by name, and the tool's own comes back when the component
unmounts.

See [The cursor](../annotations/tools#the-cursor) for annotation tools.

## Touch

On a touch screen, the viewer behaves like a native app:

- **One finger scrolls**, and two fingers pinch-zoom, with `pointer` and `pan`.
- **Drawing tools take one finger**: it draws, and two fingers still scroll and zoom.
- **Click tools**, such as placing a note, place with a tap, and a drag still scrolls.
- **Long-press selects a word**, and handles grow the selection.

A tool of your own chooses with `touch`: `'draw'` for tools you drag with, `'tap'` for tools you
click with.

## Haptic feedback

Register a feedback provider, and a long-press that lands on a word gives the small tick people know
from native apps:

**`haptics.tsx`**

```tsx
import { feedbackPlugin, interactionPlugin, vibrationFeedback } from '@embedpdf/react/interaction';

export const plugins = [/* … */ interactionPlugin(), feedbackPlugin({ provider: vibrationFeedback })];
```

It's safe to leave in: where the browser can't vibrate, iOS Safari included, nothing happens. In a
native app shell, pass `wkFeedback('yourHandlerName')` to use the app's own haptics.

## Methods

Everything `useInteraction()` gives you.

| Method                                                        | What it does                                                                                                                                                                          |
| :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `activateTool(id)`                                            | Makes a tool the active one. An id that isn't registered is refused. [The built-in tools](#the-built-in-tools)                                                                        |
| `pushTool(id)`, `popTool()`                                   | Switches to a tool and remembers the one before; goes back to it. [Hold a tool for a moment](#hold-a-tool-for-a-moment)                                                               |
| `activateDefaultTool()`                                       | Goes back to the default tool                                                                                                                                                         |
| `getDefaultToolId()`                                          | The tool a document opens with, `pointer` unless you set another                                                                                                                      |
| `registerTool(tool, { replace? })`                            | Adds a tool of your own, and returns a function that removes it. An id that's taken is refused unless you pass `replace: true`. [Your own tool](#your-own-tool)                       |
| `setToolCursor(id, cursors)`                                  | Replaces a tool's cursors by name while the app runs; `null` brings its own back. In a component, use `useToolCursor()`, which undoes it when the component goes. [Cursors](#cursors) |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useInteractionSettings()`. [Settings](#settings)                       |

## State

Everything `useInteractionState()` gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.

| Field          | What it is                                                                          | Read once                                                    |
| :------------- | :---------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `activeToolId` | The active tool's id                                                                | `getActiveToolId()`, or `getActiveTool()` for the whole tool |
| `tools`        | Every tool you can switch to, in the order they were added, the built-in ones first | `listTools()`, or `getTool(id)` and `hasTool(id)` for one    |

## Settings

Set them when you register the plugin, and change them while the app runs with
`interaction.updateSettings({ … })`; `interaction.resetSettings()` goes back to what you registered, and
`useInteractionSettings()` gives you the current settings.

| Setting       | What it does                                 | Default     |
| :------------ | :------------------------------------------- | :---------- |
| `defaultTool` | The tool that's active when a document opens | `'pointer'` |
| `tools`       | Your own tools, registered from the start    | none        |

## Events

**`tool-events.tsx`**

```tsx
import { useInteractionEvent } from '@embedpdf/react/interaction';
import { analytics } from './analytics';

export function ToolAnalytics() {
  useInteractionEvent(
    (interaction) => interaction.onToolChanged,
    ({ toolId }) => analytics.track('tool', { toolId }),
  );
  return null;
}
```

| Event                | Fires when                                       | Carries                         |
| :------------------- | :----------------------------------------------- | :------------------------------ |
| `onToolChanged`      | Another tool became active                       | `toolId`, `previousToolId`      |
| `onGestureStarted`   | A drag with a tool began                         | `toolId`, `page`, `pointerType` |
| `onGestureEnded`     | It ended                                         | `toolId`, `page`, `pointerType` |
| `onGestureCancelled` | It was cancelled, for example by a second finger | `toolId`, `page`, `pointerType` |
| `onSettingsChanged`  | The settings changed                             | `settings`, `changed`           |

## Good to know

### Each document has its own tool

The active tool belongs to the document: switch tabs, and each document is in the tool you left it in.

### Permissions

Tools don't check permissions themselves; the features behind them do. A drawing tool without
permission to create draws nothing. Hide the buttons with each plugin's checks, such as
`annotation.canCreate()`.
