# Circle annotate

> A pen circles the word that matters.

- Type: Component (draw)
- Page: https://textlab.dev/components/circle-annotate
- Markdown: https://textlab.dev/components/circle-annotate.md
- Registry item: https://textlab.dev/r/circle-annotate.json
- Source file: `registry/textlab/circle-annotate/circle-annotate.tsx`
- Dependencies: motion
- Variants: circle, box, bracket
- Keywords: circle, annotation, hand drawn, highlight word, rough notation, box

## When to use

- Pointing at the one word that sells the sentence.
- Pricing tiers, comparisons and testimonials.

## When not to use

- Several marks in one view; each one weakens the others.
- Buttons and links.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/circle-annotate
pnpm dlx shadcn@latest add @textlab/circle-annotate
```

The `@textlab` name needs `"registries": { "@textlab": "https://textlab.dev/r/{name}.json" }` in `components.json`. Without it, use the full URL:

```bash
npx shadcn@latest add https://textlab.dev/r/circle-annotate.json
```

Import the tokens once in your root layout: `import "@/components/textlab/tokens.css";`

### Manual

1. Install the dependencies: `npm install motion@12`
2. Copy `text-fx` (https://textlab.dev/r/text-fx.json) and this component into `components/textlab/`.

## Usage

```tsx
import { CircleAnnotate } from "@/components/textlab/circle-annotate/circle-annotate";

export function Example() {
  return <p>The only plan that <CircleAnnotate>scales</CircleAnnotate> with you</p>;
}
```

## API reference

### CircleAnnotate

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` | – | The word or phrase to mark. |
| `variant` | `"circle" \| "box" \| "bracket"` | `"circle"` | Loop around it, frame it, or bracket it. |
| `color` | `string` | `var(--tl-accent)` | Pen color. |
| `thickness` | `number` | `2.5` | Stroke width in px. |
| `padding` | `number` | `0.25` | Room around the word in em. |
| `as` | `ElementType` | `"span"` | Rendered element. |
| `trigger` | `"mount" \| "inView" \| "hover" \| "manual"` | `"inView"` | What starts the animation. manual follows `active`. |
| `active` | `boolean` | `false` | Drives trigger="manual". |
| `once` | `boolean` | `true` | In view: play once instead of on every entry. |
| `delay` | `number` | `0` | Seconds before the first part moves. |
| `duration` | `number` | `0.9` | Seconds each part takes. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |

## Accessibility

- Decorative SVG; the text is untouched.
- Add <strong> or <mark> if the emphasis must be conveyed to assistive tech.

## Motion

- One continuous stroke with an overshoot, drawn with the standard ease.

## Reduced motion

The mark is drawn from the start.

## Performance

- One SVG path animated with pathLength.

## Notes for AI

- Use only the props documented above. Do not invent props.
- Keep the screen-reader text and the reduced-motion branch when editing the source.
- Pick a variant from: circle, box, bracket.

## Related

- [Underline draw](https://textlab.dev/components/underline-draw.md): A hand-drawn underline sketches in.
- [Marker highlight](https://textlab.dev/components/marker-highlight.md): A highlighter swipes behind the words.
- [Strike-through](https://textlab.dev/components/strike-through.md): Cross out a word and write the new one.

## Also in draw

- [Outline draw](https://textlab.dev/components/outline-draw.md): The outline draws itself, then fills.
- [Signature](https://textlab.dev/components/signature.md): A handwritten signature writes itself.

## Source

### components/textlab/circle-annotate/circle-annotate.tsx

```tsx
"use client";

import type { CSSProperties, ElementType, ReactNode } from "react";
import { motion } from "motion/react";
import { drawProps } from "../lib/draw";
import { hashString } from "../lib/math";
import { roughBox, roughBrackets, roughEllipse } from "../lib/rough";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger, type Trigger } from "../lib/use-trigger";

export type CircleAnnotateVariant = "circle" | "box" | "bracket";

export interface CircleAnnotateProps {
  children: ReactNode;
  /** circle loops around the word, box frames it, bracket sets it off at both ends. */
  variant?: CircleAnnotateVariant;
  color?: string;
  thickness?: number;
  /** Extra room around the word, in em. */
  padding?: number;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  duration?: number;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const PATHS = { circle: roughEllipse, box: roughBox, bracket: roughBrackets };

/** A pen circles the word that matters, in one continuous stroke that overshoots its start. */
export function CircleAnnotate({ children, variant = "circle", color = "var(--tl-accent, #3a9a5b)", thickness = 2.5, padding = 0.25, as: Tag = "span", trigger = "inView", active, once = true, delay = 0, duration = 0.9, reducedMotion = "user", className, style }: CircleAnnotateProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const reduced = useReduced(reducedMotion);
  const d = PATHS[variant](hashString(typeof children === "string" ? children : variant));
  const draw = drawProps(on, { duration, delay, ease: "standard" });
  const pad = `${padding}em`;
  // An ellipse drawn inside the word's box would cut its corners, so the circle's box grows past the
  // word on every side until the corners of the letters sit inside the curve.
  const frame: CSSProperties =
    variant === "circle"
      ? { left: `calc(-12.5% - ${pad})`, top: `calc(-17.5% - 0.5 * ${pad})`, width: `calc(125% + 2 * ${pad})`, height: `calc(135% + ${pad})` }
      : { left: `calc(-1 * ${pad})`, top: `calc(-0.6 * ${pad})`, width: `calc(100% + 2 * ${pad})`, height: `calc(100% + 1.2 * ${pad})` };
  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "inline-block", ...style }} {...bind}>
      {children}
      <svg
        aria-hidden="true"
        viewBox="0 0 100 20"
        preserveAspectRatio="none"
        style={{ position: "absolute", ...frame, overflow: "visible", pointerEvents: "none" }}
      >
        <motion.path d={d} fill="none" stroke={color} strokeWidth={thickness} strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" {...(reduced ? { initial: false, animate: { pathLength: 1, opacity: 1 } } : draw)} />
      </svg>
    </Tag>
  );
}

export default CircleAnnotate;
```

Full library index: https://textlab.dev/llms.txt
