# Link preview

A link with a preview of the page it leads to, shown when the pointer rests on it or it takes keyboard focus: the page's title, a line of description, its site and address, and an image when the app has one. The preview is extra detail only; the link works without it and says where it goes in its own words.

Everything in the preview is another site's text, fetched by the app (an unfurl, the page's Open Graph tags), so it is drawn as plain text, never as HTML, each part in an isolate with bidi controls marked, and the address is shown as text, so a title can't pretend to be somewhere else: its scheme and host are shown as the browser reads them (lower-cased, an international name as punycode, with no user name or password before the host, which is noted when there is one) whole, wrapping if they must, and only the path after them is cut short. `site` is the page's own claim; the host is where the link goes. The link itself is the app's: pass its text as `children`, or the app's own link element (a router's `Link`) with its `href`.

- Registry name: `link-preview` (`@candor/link-preview`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/link-preview

## Install

```bash
npx candor-ui add link-preview
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/link-preview`.
It brings in: bidi, hover-card, locale, motion, tokens, utils.

## Example

```tsx
"use client"

import { LinkPreview } from "@/components/candor/link-preview"

// The preview's words are the other site's, fetched by the app (an unfurl): drawn as text, with the address shown.
export default function Example() {
  return (
    <p className="max-w-md text-sm">
      The agent used{" "}
      <LinkPreview
        href="https://www.rfc-editor.org/rfc/rfc9110"
        site="RFC Editor"
        title="RFC 9110: HTTP Semantics"
        description="The overall architecture of HTTP, its common terminology, and the parts of the protocol shared by all versions."
      >
        the HTTP semantics standard
      </LinkPreview>{" "}
      to check the retry rules.
    </p>
  )
}
```

## <LinkPreview>

Props:

- `href: string`: Where the link goes. Shown in the preview as text.
- `title: string`: The page's title, as the other site gives it.
- `description?: string | undefined`: A line about the page, as the other site gives it.
- `image?: string | undefined`: An image of the page (its Open Graph image), drawn as decoration. Fetch it through your own server if its address shouldn't reach the viewer's browser.
- `site?: string | undefined`: The site's name as the page claims it (its Open Graph `site_name`): "GitHub", "The Guardian". The host shown under it is where the link really goes.
- `openDelay?: number | undefined`: How long the pointer rests on the link before the preview opens, in milliseconds.
- `className?: string | undefined`: Classes for the preview card.
- `children: React.ReactNode`: The link's text, or the app's own link element (it keeps its `href`).

## Strings (`linkPreview`)

The fixed words, English by default. Override any of them under `linkPreview` in `<LocaleProvider strings={{ linkPreview: { … } }}>`; the table is exported as `linkPreviewStrings`.

- `userinfo`: This address has a name before its host, which can make it look like another site. It goes to the host shown.

## Motion

- **open** (overlay, scale): The preview opens from the link on hover or focus and closes when both leave.

Set with `--cd-motion-link-preview-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
