# Tern: documentation

> A five-page documentation site: search-first landing, a guide with synced language tabs, filterable API reference, search with highlighted matches and a changelog, with ⌘K search in the header.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/template-docs).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/template-docs
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/template-docs-light-3c896d31a91aed0a.png
- Registry item: https://ui.ballmac.com/r/template-docs.json
- Categories: templates
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://ui.ballmac.com/r/template-docs.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://ui.ballmac.com/r/command.json`, `https://ui.ballmac.com/r/kbd.json`, `https://ui.ballmac.com/r/sheet.json`, `https://ui.ballmac.com/r/snippet-tabs.json`, `https://ui.ballmac.com/r/install-tabs.json`, `https://ui.ballmac.com/r/code-block.json`, `https://ui.ballmac.com/r/callout.json`, `https://ui.ballmac.com/r/copy-button.json`, `https://ui.ballmac.com/r/table-of-contents.json`, `https://ui.ballmac.com/r/api-endpoint.json`

## Notes from the registry

Pages are at /docs, /docs/guides, /docs/reference, /docs/search and /docs/changelog. Edit navigation, search entries, releases and endpoints in docs-data.ts; edit docsCss in docs-theme.tsx for the palette. Language tabs share one storage key (tern-lang), so a reader's choice follows them across pages.

## Files

### `registry/ballmac/components/templates/docs/docs-fonts.ts`

```ts
// Ballmac UI: Docs template fonts. https://ui.ballmac.com/templates/template-docs
import { Instrument_Sans, JetBrains_Mono, Newsreader } from "next/font/google"

export const docsSerif = Newsreader({ variable: "--docs-serif", subsets: ["latin"], display: "swap", preload: false })
export const docsSans = Instrument_Sans({ variable: "--docs-sans", subsets: ["latin"], display: "swap", preload: false })
export const docsMono = JetBrains_Mono({ variable: "--docs-mono", subsets: ["latin"], display: "swap", preload: false })
```

### `registry/ballmac/components/templates/docs/docs-data.ts`

```ts
// Ballmac UI: Docs template data. https://ui.ballmac.com/templates/template-docs
export type DocsKind = "Guide" | "API" | "Changelog"

export type DocsEntry = { title: string; kind: DocsKind; path: string; summary: string; page: "guide" | "reference" | "changelog" }

/** Everything the search page and the command menu can find. */
export const searchIndex: DocsEntry[] = [
  { title: "Send your first message", kind: "Guide", path: "Get started", summary: "Install the SDK, create a queue, send a message and acknowledge it in about seven minutes.", page: "guide" },
  { title: "Delivery guarantees", kind: "Guide", path: "Concepts", summary: "At-least-once delivery, visibility timeouts and how a message is retried before it reaches the dead-letter queue.", page: "guide" },
  { title: "Dead-letter queues", kind: "Guide", path: "Concepts", summary: "Park messages that keep failing, inspect them and replay them back into the queue once the bug is fixed.", page: "guide" },
  { title: "Idempotent consumers", kind: "Guide", path: "Guides", summary: "Use the message id as an idempotency key so a retried message never charges a card twice.", page: "guide" },
  { title: "Scheduling and delays", kind: "Guide", path: "Guides", summary: "Deliver a message in thirty seconds or next Tuesday with delay and a cron-style schedule.", page: "guide" },
  { title: "Verify webhook signatures", kind: "Guide", path: "Guides", summary: "Check the Tern-Signature header with an HMAC before you trust a push delivery.", page: "guide" },
  { title: "Create a queue", kind: "API", path: "POST /v1/queues", summary: "Creates a queue with a name, a visibility timeout and an optional dead-letter target.", page: "reference" },
  { title: "List queues", kind: "API", path: "GET /v1/queues", summary: "Returns every queue in the project, newest first, with a cursor for the next page.", page: "reference" },
  { title: "Send a message", kind: "API", path: "POST /v1/queues/{queue}/messages", summary: "Adds a message to a queue. Pass an idempotency key to make retries safe.", page: "reference" },
  { title: "Receive messages", kind: "API", path: "GET /v1/queues/{queue}/messages", summary: "Long-polls a queue and hides each message from other consumers until its visibility timeout ends.", page: "reference" },
  { title: "Acknowledge a message", kind: "API", path: "DELETE /v1/queues/{queue}/messages/{id}", summary: "Deletes a message once it has been processed successfully.", page: "reference" },
  { title: "Purge a queue", kind: "API", path: "POST /v1/queues/{queue}/purge", summary: "Removes every waiting message. This cannot be undone.", page: "reference" },
  { title: "3.2: Message replay", kind: "Changelog", path: "September 2026", summary: "Replay any window of the last 14 days into a queue without touching producers.", page: "changelog" },
  { title: "3.1: Batch receive", kind: "Changelog", path: "August 2026", summary: "Receive up to 50 messages per call and acknowledge them in one request.", page: "changelog" },
  { title: "3.0: Regions", kind: "Changelog", path: "June 2026", summary: "Queues can now live in Frankfurt, Virginia or Singapore. Breaking: the default region is explicit.", page: "changelog" },
]

export type NavSection = { title: string; items: { title: string; page: "guide" | "reference" | "changelog"; current?: boolean }[] }

export const nav: NavSection[] = [
  { title: "Get started", items: [{ title: "Send your first message", page: "guide", current: true }, { title: "Core concepts", page: "guide" }, { title: "Authentication", page: "reference" }] },
  { title: "Concepts", items: [{ title: "Delivery guarantees", page: "guide" }, { title: "Visibility timeouts", page: "guide" }, { title: "Dead-letter queues", page: "guide" }] },
  { title: "Guides", items: [{ title: "Idempotent consumers", page: "guide" }, { title: "Scheduling and delays", page: "guide" }, { title: "Verify webhook signatures", page: "guide" }, { title: "Going to production", page: "guide" }] },
  { title: "API reference", items: [{ title: "Queues", page: "reference", current: true }, { title: "Messages", page: "reference" }, { title: "Errors and limits", page: "reference" }] },
  { title: "Releases", items: [{ title: "Changelog", page: "changelog", current: true }] },
]

export type Release = { version: string; date: string; title: string; tag: "Added" | "Improved" | "Fixed" | "Breaking"; body: string; points: string[] }

export const releases: Release[] = [
  { version: "3.2.0", date: "2026-09-24", title: "Message replay", tag: "Added", body: "Replay a window of past messages into any queue. Useful after a bad deploy, or to seed a new environment with real traffic.", points: ["Replay up to 14 days of history by time range or by message id", "Replays are rate limited per queue so they never starve live traffic", "New `tern replay` command and `POST /v1/queues/{queue}/replays`"] },
  { version: "3.1.4", date: "2026-09-10", title: "Faster long polling", tag: "Improved", body: "Receive calls now wake within 40 ms of a message arriving instead of waiting for the next poll tick.", points: ["Median receive latency fell from 180 ms to 46 ms", "No change needed: the improvement applies to every SDK"] },
  { version: "3.1.3", date: "2026-08-29", title: "Visibility timeout on batch receive", tag: "Fixed", body: "Messages received in a batch of 50 could become visible again early if the first acknowledgement was slow.", points: ["The visibility timer now starts per message, not per batch", "Affected SDKs: node 3.1.0 to 3.1.2, python 3.1.0 to 3.1.1"] },
  { version: "3.1.0", date: "2026-08-12", title: "Batch receive and acknowledge", tag: "Added", body: "Receive up to 50 messages per call and acknowledge them in a single request. Throughput on busy queues roughly doubles.", points: ["`maxMessages` on receive, `ids` on acknowledge", "Partial failures return a per-message result", "SDK helpers `queue.consume()` batch for you"] },
  { version: "3.0.0", date: "2026-06-03", title: "Regions are explicit", tag: "Breaking", body: "Queues now live in a region you choose. The old global endpoint is retired, so clients must set a region.", points: ["Set `region` when creating the client: `eu`, `us` or `ap`", "Existing queues were migrated to `us` with no downtime", "The global endpoint returns 410 on 1 December 2026"] },
  { version: "2.9.2", date: "2026-05-14", title: "Clearer error messages", tag: "Improved", body: "Every error now includes a stable `code`, a human sentence and a link to the page that explains the fix.", points: ["`queue_not_found`, `visibility_expired` and 14 more codes documented", "Errors in the dashboard link straight to the docs"] },
]

export const languages = ["Node", "Python", "Go", "cURL"] as const

export const endpointGroups = [
  {
    title: "Queues",
    endpoints: [
      {
        method: "POST" as const, path: "/v1/queues", summary: "Create a queue", description: "Creates a queue in the region of the client. Names are unique per project.",
        parameters: [
          { name: "name", in: "body" as const, type: "string", required: true, description: "Lowercase letters, numbers and dashes, up to 64 characters." },
          { name: "visibilityTimeout", in: "body" as const, type: "integer", description: "Seconds a received message stays hidden.", default: "30" },
          { name: "deadLetter", in: "body" as const, type: "string", description: "Name of the queue that receives messages after maxReceives failures." },
          { name: "maxReceives", in: "body" as const, type: "integer", description: "Receives before a message is moved to the dead-letter queue.", default: "5" },
        ],
        requestExample: `{\n  "name": "invoices",\n  "visibilityTimeout": 60,\n  "deadLetter": "invoices-dead",\n  "maxReceives": 5\n}`,
        responses: [
          { status: 201, description: "Queue created", example: `{\n  "name": "invoices",\n  "region": "eu",\n  "visibilityTimeout": 60,\n  "createdAt": "2026-09-30T08:14:22Z"\n}` },
          { status: 409, description: "A queue with this name already exists", example: `{\n  "code": "queue_exists",\n  "message": "A queue named invoices already exists in eu."\n}` },
        ],
      },
      {
        method: "GET" as const, path: "/v1/queues", summary: "List queues", description: "Returns the queues in the project, newest first.",
        parameters: [
          { name: "limit", in: "query" as const, type: "integer", description: "Queues per page, up to 100.", default: "25" },
          { name: "cursor", in: "query" as const, type: "string", description: "The `next` value from the previous page." },
        ],
        responses: [{ status: 200, description: "A page of queues", example: `{\n  "data": [{ "name": "invoices", "waiting": 12, "inFlight": 3 }],\n  "next": null\n}` }],
      },
      {
        method: "POST" as const, path: "/v1/queues/{queue}/purge", summary: "Purge a queue", description: "Removes every waiting message. In-flight messages finish normally. This cannot be undone.",
        parameters: [{ name: "queue", in: "path" as const, type: "string", required: true, description: "The queue name." }],
        responses: [{ status: 202, description: "Purge accepted", example: `{ "removed": 1204 }` }, { status: 404, description: "No such queue", example: `{ "code": "queue_not_found" }` }],
      },
    ],
  },
  {
    title: "Messages",
    endpoints: [
      {
        method: "POST" as const, path: "/v1/queues/{queue}/messages", summary: "Send a message", description: "Adds a message to the queue. Send an `Idempotency-Key` header and a retry will return the first result instead of adding a duplicate.",
        parameters: [
          { name: "queue", in: "path" as const, type: "string", required: true, description: "The queue name." },
          { name: "Idempotency-Key", in: "header" as const, type: "string", description: "Any string up to 255 characters. Keys expire after 24 hours." },
          { name: "body", in: "body" as const, type: "object", required: true, description: "Any JSON up to 256 KB." },
          { name: "delay", in: "body" as const, type: "integer", description: "Seconds before the message becomes visible, up to 15 minutes.", default: "0" },
        ],
        requestExample: `{\n  "body": { "invoice": "inv_2041", "total": 4200 },\n  "delay": 0\n}`,
        responses: [{ status: 201, description: "Message accepted", example: `{\n  "id": "msg_7Hq2Zc",\n  "queue": "invoices",\n  "visibleAt": "2026-09-30T08:14:22Z"\n}` }, { status: 413, description: "Body is larger than 256 KB", example: `{ "code": "body_too_large" }` }],
      },
      {
        method: "GET" as const, path: "/v1/queues/{queue}/messages", summary: "Receive messages", description: "Long-polls for up to `wait` seconds. Each message is hidden from other consumers until its visibility timeout ends.",
        parameters: [
          { name: "queue", in: "path" as const, type: "string", required: true, description: "The queue name." },
          { name: "maxMessages", in: "query" as const, type: "integer", description: "Up to 50 per call.", default: "1" },
          { name: "wait", in: "query" as const, type: "integer", description: "Seconds to wait for a message, up to 20.", default: "0" },
        ],
        responses: [{ status: 200, description: "Zero or more messages", example: `{\n  "data": [\n    { "id": "msg_7Hq2Zc", "receipt": "rcpt_91", "body": { "invoice": "inv_2041" }, "receives": 1 }\n  ]\n}` }],
      },
      {
        method: "DELETE" as const, path: "/v1/queues/{queue}/messages/{id}", summary: "Acknowledge a message", description: "Deletes a message once it has been processed. Use the receipt from the receive call.",
        parameters: [
          { name: "queue", in: "path" as const, type: "string", required: true, description: "The queue name." },
          { name: "id", in: "path" as const, type: "string", required: true, description: "The message id." },
          { name: "receipt", in: "query" as const, type: "string", required: true, description: "Proves you hold the message right now." },
        ],
        responses: [{ status: 204, description: "Deleted" }, { status: 410, description: "The visibility timeout ended; the message may already be with another consumer", example: `{ "code": "visibility_expired" }` }],
      },
    ],
  },
]

export const snippets = {
  install: { Node: "npm install @tern/sdk", Python: "pip install tern", Go: "go get github.com/tern/tern-go", cURL: "# nothing to install" },
}
```

### `registry/ballmac/components/templates/docs/docs-theme.tsx`

```tsx
// Ballmac UI: Docs template shell. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { ChevronRight, Menu, Search } from "lucide-react"

import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ballmac/command"
import { Kbd } from "@/components/ballmac/kbd"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ballmac/sheet"
import { nav, searchIndex, type DocsKind } from "@/components/ballmac/templates/docs/docs-data"
import { docsMono, docsSans, docsSerif } from "@/components/ballmac/templates/docs/docs-fonts"
import { cn } from "@/lib/utils"

type DocsPage = "home" | "guide" | "reference" | "search" | "changelog"
type DocsHrefs = Record<DocsPage, string>

const defaultHrefs: DocsHrefs = { home: "/docs", guide: "/docs/guides", reference: "/docs/reference", search: "/docs/search", changelog: "/docs/changelog" }

/** Tern's palette: a cool, quiet page with deep teal ink. Dark mode is the same page lit by a terminal. */
const docsCss = `
.docs-theme,body:not(:has([data-gallery])):has(.docs-theme){--background:oklch(0.985 0.006 210);--foreground:oklch(0.2 0.025 235);--card:oklch(1 0 0);--card-foreground:oklch(0.2 0.025 235);--popover:oklch(1 0 0);--popover-foreground:oklch(0.2 0.025 235);--primary:oklch(0.4 0.09 200);--primary-foreground:oklch(0.985 0.006 210);--secondary:oklch(0.955 0.012 210);--secondary-foreground:oklch(0.2 0.025 235);--muted:oklch(0.955 0.012 210);--muted-foreground:oklch(0.46 0.03 235);--accent:oklch(0.945 0.02 200);--accent-foreground:oklch(0.2 0.025 235);--border:oklch(0.2 0.025 235 / 12%);--input:oklch(0.2 0.025 235 / 18%);--ring:oklch(0.5 0.11 195);--surface:oklch(0.97 0.01 210);--destructive:oklch(0.52 0.21 27);--chart-1:oklch(0.5 0.11 190);--chart-2:oklch(0.5 0.15 265);--chart-3:oklch(0.62 0.15 75);--chart-4:oklch(0.55 0.15 150);--chart-5:oklch(0.55 0.2 20);--radius:0.75rem}
.dark .docs-theme,.dark body:not(:has([data-gallery])):has(.docs-theme){--background:oklch(0.165 0.02 235);--foreground:oklch(0.95 0.01 205);--card:oklch(0.2 0.022 235);--card-foreground:oklch(0.95 0.01 205);--popover:oklch(0.22 0.024 235);--popover-foreground:oklch(0.95 0.01 205);--primary:oklch(0.8 0.11 190);--primary-foreground:oklch(0.18 0.03 235);--secondary:oklch(0.25 0.025 235);--secondary-foreground:oklch(0.95 0.01 205);--muted:oklch(0.25 0.025 235);--muted-foreground:oklch(0.72 0.03 215);--accent:oklch(0.28 0.03 225);--accent-foreground:oklch(0.95 0.01 205);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 15%);--ring:oklch(0.8 0.11 190);--surface:oklch(0.19 0.021 235);--destructive:oklch(0.7 0.19 27);--chart-1:oklch(0.8 0.11 190);--chart-2:oklch(0.76 0.12 265);--chart-3:oklch(0.82 0.13 80);--chart-4:oklch(0.78 0.13 150);--chart-5:oklch(0.74 0.14 20)}
body:not(:has([data-gallery])):has(.docs-theme){font-family:var(--docs-sans),ui-sans-serif,system-ui,sans-serif}
@keyframes docs-packet{0%{transform:translateX(0);opacity:0}10%{opacity:1}90%{opacity:1}100%{transform:translateX(var(--docs-travel,10rem));opacity:0}}
.docs-packet{animation:docs-packet 2.6s ease-in-out infinite;animation-play-state:var(--bm-ambient-state,running)}
@media (prefers-reduced-motion:reduce){.docs-packet{animation:none;opacity:1}}
`

const serif = "[font-family:var(--docs-serif),ui-serif,Georgia,serif] font-medium tracking-[-0.015em]"
const mono = "[font-family:var(--docs-mono),ui-monospace,monospace]"

const kindStyle: Record<DocsKind, string> = { Guide: "bg-chart-1/15", API: "bg-chart-2/15", Changelog: "bg-chart-3/20" }

function Logo({ className }: { className?: string }) {
  return (
    <span aria-hidden="true" className={cn("bg-primary text-primary-foreground inline-flex size-8 items-center justify-center rounded-lg", className)}>
      <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 8h9a4 4 0 0 1 0 8H8" /><path d="m11 13-3 3 3 3" /></svg>
    </span>
  )
}

type SidebarProps = { page: DocsPage; hrefs: DocsHrefs; onNavigate?: () => void }
function SidebarNav({ page, hrefs, onNavigate }: SidebarProps) {
  return (
    <nav aria-label="Documentation" className="grid gap-6 text-sm">
      {nav.map((section) => (
        <div key={section.title}>
          <h2 className="text-foreground mb-2 px-2 text-xs font-bold tracking-[0.12em] uppercase">{section.title}</h2>
          <ul className="grid gap-0.5">
            {section.items.map((item) => {
              const current = item.page === page && !!item.current
              return (
                <li key={item.title}>
                  <a
                    href={hrefs[item.page]}
                    onClick={onNavigate}
                    aria-current={current ? "page" : undefined}
                    className="text-muted-foreground hover:text-foreground aria-[current=page]:bg-accent aria-[current=page]:text-foreground focus-visible:ring-ring/50 relative block rounded-md px-2 py-1.5 outline-none transition-colors focus-visible:ring-[3px] aria-[current=page]:font-semibold motion-reduce:transition-none"
                  >
                    {item.title}
                  </a>
                </li>
              )
            })}
          </ul>
        </div>
      ))}
    </nav>
  )
}

type DocsShellProps = React.ComponentProps<"div"> & {
  /** The page being shown, so its nav link is marked current. */
  page: DocsPage
  /** Override where pages live (used by previews). */
  hrefs?: Partial<DocsHrefs>
  /** Show the left navigation. Landing and search pages turn it off. */
  sidebar?: boolean
}

/** Tern's frame: a sticky header with the ⌘K search, a version picker, a sidebar and a footer. */
function DocsShell({ page, hrefs: overrides, sidebar = true, className, style, children, ...props }: DocsShellProps) {
  const hrefs = { ...defaultHrefs, ...overrides }
  const [searchOpen, setSearchOpen] = React.useState(false)
  const [menuOpen, setMenuOpen] = React.useState(false)
  React.useEffect(() => {
    const classes = [docsSerif.variable, docsSans.variable, docsMono.variable].filter(Boolean)
    document.body.classList.add(...classes)
    return () => document.body.classList.remove(...classes)
  }, [])
  return (
    <div
      data-slot="docs"
      className={cn("docs-theme bg-background text-foreground relative min-h-dvh overflow-x-clip", docsSerif.variable, docsSans.variable, docsMono.variable, className)}
      style={{ fontFamily: "var(--docs-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
      {...props}
    >
      <style>{docsCss}</style>
      <header className="bg-background/85 sticky top-0 z-40 border-b backdrop-blur-xl">
        <div className="mx-auto flex h-16 max-w-[90rem] items-center gap-3 px-4 sm:px-6">
          {sidebar && (
            <Sheet open={menuOpen} onOpenChange={setMenuOpen}>
              <SheetTrigger className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px] lg:hidden" aria-label="Open navigation"><Menu className="size-5" aria-hidden="true" /></SheetTrigger>
              <SheetContent side="start" className="docs-theme bg-background w-80 overflow-y-auto p-5">
                <SheetHeader className="p-0 pb-4"><SheetTitle>Tern docs</SheetTitle><SheetDescription>Browse every guide and the API reference.</SheetDescription></SheetHeader>
                <SidebarNav page={page} hrefs={hrefs} onNavigate={() => setMenuOpen(false)} />
              </SheetContent>
            </Sheet>
          )}
          <a href={hrefs.home} className="focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-lg outline-none focus-visible:ring-[3px]">
            <Logo />
            <span className={cn("text-2xl", serif)}>Tern</span>
            <span className="text-muted-foreground hidden border-s ps-2.5 text-sm font-semibold sm:inline">Docs</span>
          </a>
          <nav aria-label="Main" className="ms-4 hidden items-center gap-1 text-sm font-medium md:flex">
            {([["guide", "Guides"], ["reference", "API reference"], ["changelog", "Changelog"]] as const).map(([key, label]) => (
              <a key={key} href={hrefs[key]} aria-current={page === key ? "page" : undefined} className="text-muted-foreground hover:text-foreground aria-[current=page]:text-foreground focus-visible:ring-ring/50 rounded-md px-3 py-2 outline-none transition-colors focus-visible:ring-[3px] aria-[current=page]:font-semibold motion-reduce:transition-none">{label}</a>
            ))}
          </nav>
          <div className="ms-auto flex items-center gap-2">
            <button type="button" onClick={() => setSearchOpen(true)} className="bg-surface hover:bg-accent focus-visible:ring-ring/50 text-muted-foreground inline-flex h-10 items-center gap-2 rounded-lg border px-3 text-sm outline-none transition-colors focus-visible:ring-[3px] sm:w-64 motion-reduce:transition-none" aria-label="Search the docs">
              <Search className="size-4 shrink-0" aria-hidden="true" />
              <span className="hidden flex-1 text-start sm:inline">Search the docs</span>
              <span className="hidden items-center gap-1 sm:inline-flex" aria-hidden="true"><Kbd>⌘</Kbd><Kbd>K</Kbd></span>
            </button>
            <label className="sr-only" htmlFor="docs-version">API version</label>
            <select id="docs-version" defaultValue="v3" className="bg-background focus-visible:ring-ring/50 h-10 rounded-lg border px-2.5 text-sm font-medium outline-none focus-visible:ring-[3px]">
              <option value="v3">v3.2</option>
              <option value="v2">v2.9</option>
            </select>
          </div>
        </div>
      </header>

      <CommandDialog open={searchOpen} onOpenChange={setSearchOpen} title="Search the docs" description="Type to search guides, the API reference and the changelog.">
        <Command>
          <CommandInput placeholder="Search guides, endpoints and releases" />
          <CommandList>
            <CommandEmpty>No results. Try “queue” or “retry”.</CommandEmpty>
            {(["Guide", "API", "Changelog"] as const).map((kind) => (
              <CommandGroup key={kind} heading={kind === "API" ? "API reference" : kind === "Guide" ? "Guides" : "Releases"}>
                {searchIndex.filter((e) => e.kind === kind).map((e) => (
                  <CommandItem key={e.title} value={`${e.title} ${e.path} ${e.summary}`} onSelect={() => { setSearchOpen(false); window.location.href = hrefs[e.page] }}>
                    <span className="grid min-w-0"><span className="truncate font-medium">{e.title}</span><span className={cn("text-muted-foreground truncate text-xs", kind === "API" && mono)}>{e.path}</span></span>
                  </CommandItem>
                ))}
              </CommandGroup>
            ))}
          </CommandList>
        </Command>
      </CommandDialog>

      {sidebar ? (
        <div className="mx-auto grid max-w-[90rem] grid-cols-[minmax(0,1fr)] lg:grid-cols-[16rem_minmax(0,1fr)]">
          <aside className="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto border-e p-6 pe-4 lg:block" aria-label="Sidebar">
            <SidebarNav page={page} hrefs={hrefs} />
          </aside>
          <div className="min-w-0">{children}</div>
        </div>
      ) : (
        children
      )}

      <footer className="border-t">
        <div className="mx-auto grid max-w-[90rem] gap-8 px-4 py-12 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr_1fr]">
          <div><a href={hrefs.home} className="flex items-center gap-2.5"><Logo className="size-7" /><span className={cn("text-xl", serif)}>Tern</span></a><p className="text-muted-foreground mt-3 max-w-xs text-sm text-pretty">Queues that never lose a message. Built by people who have been paged at 3 a.m.</p></div>
          {[
            ["Learn", [["Quickstart", hrefs.guide], ["Concepts", hrefs.guide], ["Guides", hrefs.guide]]],
            ["Build", [["API reference", hrefs.reference], ["SDKs", hrefs.reference], ["Changelog", hrefs.changelog]]],
            ["Help", [["Search", hrefs.search], ["Status", hrefs.home], ["Contact support", hrefs.home]]],
          ].map(([title, items]) => (
            <div key={title as string}><h2 className="text-xs font-bold tracking-[0.12em] uppercase">{title as string}</h2><ul className="mt-3 grid gap-2 text-sm">{(items as string[][]).map(([l, h]) => <li key={l}><a href={h} className="text-muted-foreground hover:text-foreground">{l}</a></li>)}</ul></div>
          ))}
        </div>
        <p className="text-muted-foreground mx-auto max-w-[90rem] border-t px-4 py-5 text-xs sm:px-6">© 2026 Tern Systems. All systems operational.</p>
      </footer>
    </div>
  )
}

/** A small crumb row: "Get started › Send your first message". */
function Crumbs({ items }: { items: string[] }) {
  return (
    <nav aria-label="Breadcrumb" className="text-muted-foreground flex flex-wrap items-center gap-1 text-sm">
      {items.map((it, i) => (
        <React.Fragment key={it}>
          {i > 0 && <ChevronRight className="size-3.5 rtl:rotate-180" aria-hidden="true" />}
          <span aria-current={i === items.length - 1 ? "page" : undefined} className={i === items.length - 1 ? "text-foreground font-medium" : undefined}>{it}</span>
        </React.Fragment>
      ))}
    </nav>
  )
}

export { Crumbs, DocsShell, Logo as DocsLogo, defaultHrefs as docsDefaultHrefs, kindStyle as docsKindStyle, mono as docsMonoClass, serif as docsSerifClass, type DocsHrefs, type DocsPage, type DocsShellProps }
```

### `registry/ballmac/components/templates/docs/docs-home.tsx`

```tsx
// Ballmac UI: Docs home page. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { ArrowRight, BookOpen, Code2, History, Rocket, Search } from "lucide-react"

import { SnippetTabs } from "@/components/ballmac/snippet-tabs"
import { releases, searchIndex, snippets } from "@/components/ballmac/templates/docs/docs-data"
import { DocsShell, docsMonoClass, docsSerifClass, type DocsHrefs } from "@/components/ballmac/templates/docs/docs-theme"
import { cn } from "@/lib/utils"

const paths = [
  { icon: Rocket, title: "Quickstart", body: "Send and receive your first message in about seven minutes.", key: "guide" as const, tint: "bg-chart-1/15" },
  { icon: BookOpen, title: "Guides", body: "Retries, dead letters, schedules and idempotent consumers, explained with code.", key: "guide" as const, tint: "bg-chart-2/15" },
  { icon: Code2, title: "API reference", body: "Every endpoint, parameter and error code, with examples in four languages.", key: "reference" as const, tint: "bg-chart-3/20" },
  { icon: History, title: "Changelog", body: "What shipped, what changed and what to do about it.", key: "changelog" as const, tint: "bg-chart-4/15" },
]

/** Producer, queue and consumer with packets travelling between them. Decorative; the sentence below carries the meaning. */
function Flow() {
  const box = "bg-card grid h-20 w-24 shrink-0 place-items-center rounded-xl border text-center text-xs font-semibold shadow-xs sm:w-32 sm:text-sm"
  return (
    <figure className="mx-auto w-full max-w-xl" aria-label="How a message travels">
      <div aria-hidden="true" className="flex items-center">
        <div className={box}><span>Producer<span className={cn("text-muted-foreground block text-xs font-normal", docsMonoClass)}>your app</span></span></div>
        <div className="relative h-px min-w-6 flex-1 border-t-2 border-dashed [--docs-travel:1.75rem] sm:[--docs-travel:5rem]"><span className="docs-packet bg-chart-1 absolute -top-[7px] start-0 size-3 rounded-full" /></div>
        <div className={cn(box, "bg-primary text-primary-foreground border-primary")}><span>Queue<span className={cn("block text-xs font-normal opacity-100", docsMonoClass)}>invoices</span></span></div>
        <div className="relative h-px min-w-6 flex-1 border-t-2 border-dashed [--docs-travel:1.75rem] sm:[--docs-travel:5rem]"><span className="docs-packet bg-chart-3 absolute -top-[7px] start-0 size-3 rounded-full" style={{ animationDelay: "1.1s" }} /></div>
        <div className={box}><span>Consumer<span className={cn("text-muted-foreground block text-xs font-normal", docsMonoClass)}>worker</span></span></div>
      </div>
      <figcaption className="text-muted-foreground mt-4 text-center text-sm">A producer sends, Tern holds the message until a consumer acknowledges it.</figcaption>
    </figure>
  )
}

type DocsHomeProps = React.ComponentProps<"div"> & { hrefs?: Partial<DocsHrefs> }

/** The Docs landing page: a search-first hero, four paths in, install tabs and the latest releases. */
function DocsHome({ hrefs, ...props }: DocsHomeProps) {
  const link = { ...{ home: "/docs", guide: "/docs/guides", reference: "/docs/reference", search: "/docs/search", changelog: "/docs/changelog" }, ...hrefs }
  return (
    <DocsShell page="home" hrefs={hrefs} sidebar={false} {...props}>
      <main>
        <section aria-labelledby="dh-title" className="relative isolate overflow-hidden border-b">
          <div aria-hidden="true" className="absolute inset-0 -z-10 [background-image:radial-gradient(var(--border)_1.2px,transparent_1.2px)] [background-size:22px_22px] [mask-image:linear-gradient(to_bottom,black,transparent_85%)]" />
          <div className="mx-auto max-w-5xl px-4 pt-16 pb-14 text-center sm:px-6 sm:pt-24">
            <p className={cn("bg-accent text-accent-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold", docsMonoClass)}><span className="bg-chart-4 size-1.5 rounded-full" aria-hidden="true" />v3.2 · message replay is here</p>
            <h1 id="dh-title" className={cn("mx-auto mt-6 max-w-3xl text-[clamp(2.8rem,7vw,5.5rem)] leading-[1.02] text-balance", docsSerifClass)}>Queues that <em className="text-primary">never lose</em> a message.</h1>
            <p className="text-muted-foreground mx-auto mt-5 max-w-xl text-lg text-pretty">Everything you need to send, receive and retry messages at any scale, written by the people who run the queues.</p>
            <form action={link.search} method="get" role="search" className="mx-auto mt-9 flex max-w-xl items-center gap-2">
              <label htmlFor="dh-q" className="sr-only">Search the docs</label>
              <div className="bg-card focus-within:ring-ring/50 relative flex-1 rounded-xl border shadow-sm focus-within:ring-[3px]">
                <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-4 size-5 -translate-y-1/2" aria-hidden="true" />
                <input id="dh-q" name="q" type="search" placeholder="Search: retries, idempotency, dead letters…" className="h-14 w-full rounded-xl bg-transparent pe-4 ps-12 text-base outline-none" />
              </div>
              <button type="submit" className="bg-primary text-primary-foreground focus-visible:ring-ring/50 h-14 rounded-xl px-6 text-base font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Search</button>
            </form>
            <p className="text-muted-foreground mt-4 text-sm">Popular: {["Retries", "Dead letters", "Idempotency"].map((t, i) => <React.Fragment key={t}>{i > 0 && ", "}<a href={`${link.search}?q=${t.toLowerCase()}`} className="text-foreground underline underline-offset-4">{t}</a></React.Fragment>)}</p>
            <div className="mt-14"><Flow /></div>
          </div>
        </section>

        <section aria-labelledby="dh-paths" className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
          <h2 id="dh-paths" className="sr-only">Where to start</h2>
          <ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
            {paths.map((p) => (
              <li key={p.title}>
                <a href={link[p.key]} className="bg-card hover:border-primary/50 focus-visible:ring-ring/50 group flex h-full flex-col rounded-2xl border p-6 outline-none transition-[border-color,transform] hover:translate-y-[var(--bm-lift,-2px)] focus-visible:ring-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
                  <span className={cn("inline-flex size-11 items-center justify-center rounded-xl", p.tint)}><p.icon className="size-5" aria-hidden="true" /></span>
                  <span className={cn("mt-5 text-2xl", docsSerifClass)}>{p.title}</span>
                  <span className="text-muted-foreground mt-2 flex-1 text-sm text-pretty">{p.body}</span>
                  <span className="text-primary mt-5 inline-flex items-center gap-1 text-sm font-semibold">Open <ArrowRight className="size-4 transition-transform group-hover:translate-x-1 motion-reduce:transition-none rtl:rotate-180 rtl:group-hover:-translate-x-1" aria-hidden="true" /></span>
                </a>
              </li>
            ))}
          </ul>
        </section>

        <section aria-labelledby="dh-install" className="bg-surface border-y">
          <div className="mx-auto grid max-w-6xl items-center gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[1fr_1.1fr]">
            <div>
              <h2 id="dh-install" className={cn("text-4xl text-balance sm:text-5xl", docsSerifClass)}>One command, then a message.</h2>
              <p className="text-muted-foreground mt-4 text-lg text-pretty">Official SDKs for Node, Python and Go, and a plain HTTP API for everything else. Pick a language once and every code sample in the docs follows you.</p>
              <ul className="mt-6 grid gap-2 text-sm">{["Retries and backoff built in", "Typed clients, generated from the API", "Same behaviour in every language"].map((t) => <li key={t} className="flex items-center gap-2"><span className="bg-chart-4 size-1.5 rounded-full" aria-hidden="true" />{t}</li>)}</ul>
            </div>
            <SnippetTabs storageKey="tern-lang" title="Install" snippets={(Object.entries(snippets.install) as [string, string][]).map(([label, code]) => ({ label, code, language: "bash" }))} />
          </div>
        </section>

        <section aria-labelledby="dh-pop" className="mx-auto grid max-w-6xl gap-12 px-4 py-16 sm:px-6 lg:grid-cols-2">
          <div>
            <h2 id="dh-pop" className={cn("text-3xl", docsSerifClass)}>Most read</h2>
            <ol className="mt-5 divide-y border-y">
              {searchIndex.filter((e) => e.kind === "Guide").slice(0, 5).map((e, i) => (
                <li key={e.title}>
                  <a href={link[e.page]} className="hover:bg-accent focus-visible:ring-ring/50 group grid grid-cols-[2rem_1fr_auto] items-center gap-3 px-2 py-4 outline-none focus-visible:ring-[3px] focus-visible:ring-inset">
                    <span className={cn("text-muted-foreground text-sm", docsMonoClass)}>{String(i + 1).padStart(2, "0")}</span>
                    <span className="min-w-0"><span className="block font-semibold">{e.title}</span><span className="text-muted-foreground block truncate text-sm">{e.summary}</span></span>
                    <ArrowRight className="text-muted-foreground size-4 transition-transform group-hover:translate-x-1 motion-reduce:transition-none rtl:rotate-180 rtl:group-hover:-translate-x-1" aria-hidden="true" />
                  </a>
                </li>
              ))}
            </ol>
          </div>
          <div>
            <div className="flex items-end justify-between"><h2 className={cn("text-3xl", docsSerifClass)}>What’s new</h2><a href={link.changelog} className="text-primary text-sm font-semibold underline-offset-4 hover:underline">All releases</a></div>
            <ul className="mt-5 grid gap-4">
              {releases.slice(0, 3).map((r) => (
                <li key={r.version} className="bg-card rounded-2xl border p-5">
                  <div className="flex items-center gap-3"><span className={cn("bg-secondary rounded-md px-2 py-0.5 text-xs font-bold", docsMonoClass)}>{r.version}</span><span className="text-muted-foreground text-xs">{new Date(r.date + "T00:00:00Z").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" })}</span></div>
                  <h3 className="mt-3 text-lg font-semibold">{r.title}</h3>
                  <p className="text-muted-foreground mt-1 text-sm text-pretty">{r.body}</p>
                </li>
              ))}
            </ul>
          </div>
        </section>

        <section aria-labelledby="dh-help" className="mx-auto max-w-6xl px-4 pb-8 sm:px-6">
          <div className="bg-primary text-primary-foreground flex flex-col items-start justify-between gap-6 rounded-3xl p-8 sm:flex-row sm:items-center sm:p-12">
            <div><h2 id="dh-help" className={cn("text-3xl sm:text-4xl", docsSerifClass)}>Stuck? A human will answer.</h2><p className="mt-2 max-w-lg text-pretty">Average first reply on weekdays is eleven minutes. Bring your message id and we will find it.</p></div>
            <a href={link.search} className="bg-primary-foreground text-primary focus-visible:ring-ring inline-flex h-12 shrink-0 items-center gap-2 rounded-xl px-6 font-semibold outline-none focus-visible:ring-[3px]">Ask support <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" /></a>
          </div>
        </section>
      </main>
    </DocsShell>
  )
}

export { DocsHome, type DocsHomeProps }
```

### `registry/ballmac/components/templates/docs/docs-guide.tsx`

```tsx
// Ballmac UI: Docs guide page. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { ArrowLeft, ArrowRight, Check, Clock, ThumbsDown, ThumbsUp } from "lucide-react"

import { Callout } from "@/components/ballmac/callout"
import { CodeBlock } from "@/components/ballmac/code-block"
import { CopyButton } from "@/components/ballmac/copy-button"
import { InstallTabs } from "@/components/ballmac/install-tabs"
import { SnippetTabs } from "@/components/ballmac/snippet-tabs"
import { TableOfContents } from "@/components/ballmac/table-of-contents"
import { Crumbs, DocsShell, docsMonoClass, docsSerifClass, type DocsHrefs } from "@/components/ballmac/templates/docs/docs-theme"
import { cn } from "@/lib/utils"

const toc = [
  { id: "before", title: "Before you begin", level: 2 },
  { id: "install", title: "Install the SDK", level: 2 },
  { id: "create", title: "Create a queue", level: 2 },
  { id: "send", title: "Send a message", level: 2 },
  { id: "receive", title: "Receive and acknowledge", level: 2 },
  { id: "next", title: "Next steps", level: 2 },
]

const createSnippets = [
  { label: "Node", language: "typescript" as const, code: `const { Tern } = require("@tern/sdk")\n\nconst tern = new Tern({ apiKey: process.env.TERN_KEY, region: "eu" })\n\nawait tern.queues.create({\n  name: "invoices",\n  visibilityTimeout: 60,\n})` },
  { label: "Python", language: "python" as const, code: `import os\nfrom tern import Tern\n\ntern = Tern(api_key=os.environ["TERN_KEY"], region="eu")\n\ntern.queues.create(\n    name="invoices",\n    visibility_timeout=60,\n)` },
  { label: "Go", language: "go" as const, code: `client := tern.New(os.Getenv("TERN_KEY"), tern.WithRegion("eu"))\n\n_, err := client.Queues.Create(ctx, tern.CreateQueue{\n    Name:              "invoices",\n    VisibilityTimeout: 60,\n})` },
  { label: "cURL", language: "bash" as const, code: `curl https://eu.api.tern.dev/v1/queues \\\n  -H "Authorization: Bearer $TERN_KEY" \\\n  -d '{"name":"invoices","visibilityTimeout":60}'` },
]
const sendSnippets = [
  { label: "Node", language: "typescript" as const, code: `const message = await tern.queue("invoices").send(\n  { invoice: "inv_2041", total: 4200 },\n  { idempotencyKey: "inv_2041" }\n)\n\nconsole.log(message.id) // msg_7Hq2Zc` },
  { label: "Python", language: "python" as const, code: `message = tern.queue("invoices").send(\n    {"invoice": "inv_2041", "total": 4200},\n    idempotency_key="inv_2041",\n)\n\nprint(message.id)  # msg_7Hq2Zc` },
  { label: "Go", language: "go" as const, code: `msg, _ := client.Queue("invoices").Send(ctx,\n    map[string]any{"invoice": "inv_2041", "total": 4200},\n    tern.IdempotencyKey("inv_2041"))\n\nfmt.Println(msg.ID) // msg_7Hq2Zc` },
  { label: "cURL", language: "bash" as const, code: `curl https://eu.api.tern.dev/v1/queues/invoices/messages \\\n  -H "Authorization: Bearer $TERN_KEY" \\\n  -H "Idempotency-Key: inv_2041" \\\n  -d '{"body":{"invoice":"inv_2041","total":4200}}'` },
]
const consumeSnippets = [
  { label: "Node", language: "typescript" as const, code: `for await (const msg of tern.queue("invoices").consume({ wait: 20 })) {\n  await chargeCustomer(msg.body)\n  await msg.ack() // delete it, or it comes back\n}` },
  { label: "Python", language: "python" as const, code: `for msg in tern.queue("invoices").consume(wait=20):\n    charge_customer(msg.body)\n    msg.ack()  # delete it, or it comes back` },
  { label: "Go", language: "go" as const, code: `for msg := range client.Queue("invoices").Consume(ctx, tern.Wait(20)) {\n    chargeCustomer(msg.Body)\n    msg.Ack() // delete it, or it comes back\n}` },
  { label: "cURL", language: "bash" as const, code: `curl "https://eu.api.tern.dev/v1/queues/invoices/messages?wait=20" \\\n  -H "Authorization: Bearer $TERN_KEY"` },
]

function Step({ n, title, id, children, last }: { n: number; title: string; id: string; children: React.ReactNode; last?: boolean }) {
  return (
    <section aria-labelledby={id} className="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-x-4 sm:gap-x-5">
      <div className="flex flex-col items-center">
        <span aria-hidden="true" className={cn("bg-primary text-primary-foreground flex size-10 items-center justify-center rounded-full text-sm font-bold", docsMonoClass)}>{n}</span>
        {!last && <span aria-hidden="true" className="bg-border mt-2 w-px flex-1" />}
      </div>
      <div className={cn("grid min-w-0 gap-4", !last && "pb-12")}>
        <h2 id={id} className={cn("scroll-mt-24 pt-1 text-3xl", docsSerifClass)}>{title}</h2>
        {children}
      </div>
    </section>
  )
}

function Helpful() {
  const [vote, setVote] = React.useState<"yes" | "no" | null>(null)
  const [note, setNote] = React.useState("")
  const [sent, setSent] = React.useState(false)
  const btn = "hover:bg-accent focus-visible:ring-ring/50 aria-pressed:border-primary aria-pressed:bg-accent inline-flex h-10 items-center gap-2 rounded-lg border px-4 text-sm font-semibold outline-none focus-visible:ring-[3px]"
  return (
    <section aria-labelledby="dg-help" className="bg-surface rounded-2xl border p-6">
      <h2 id="dg-help" className="font-semibold">Was this page helpful?</h2>
      {sent || vote === "yes" ? (
        <p role="status" className="mt-3 flex items-center gap-2 text-sm"><Check className="text-chart-4 size-4" aria-hidden="true" />{sent ? "Thanks. We read every note." : "Glad it helped. Thank you."}</p>
      ) : (
        <>
          <div className="mt-3 flex gap-2">
            <button type="button" className={btn} onClick={() => setVote("yes")}><ThumbsUp className="size-4" aria-hidden="true" />Yes</button>
            <button type="button" className={btn} aria-pressed={vote === "no"} onClick={() => setVote("no")}><ThumbsDown className="size-4" aria-hidden="true" />No</button>
          </div>
          {vote === "no" && (
            <form className="mt-4 grid gap-3" onSubmit={(e) => { e.preventDefault(); setSent(true) }}>
              <label htmlFor="dg-note" className="text-sm font-medium">What was missing or confusing?</label>
              <textarea id="dg-note" value={note} onChange={(e) => setNote(e.target.value)} rows={3} className="bg-background focus-visible:ring-ring/50 w-full rounded-lg border p-3 text-sm outline-none focus-visible:ring-[3px]" />
              <button type="submit" className="bg-primary text-primary-foreground focus-visible:ring-ring/50 h-10 w-fit rounded-lg px-5 text-sm font-semibold outline-none focus-visible:ring-[3px]">Send feedback</button>
            </form>
          )}
        </>
      )}
    </section>
  )
}

type DocsGuideProps = React.ComponentProps<"div"> & { hrefs?: Partial<DocsHrefs> }

/** A Docs guide: breadcrumbs, numbered steps with synced language tabs, callouts, an on-this-page list and a feedback box. */
function DocsGuide({ hrefs, ...props }: DocsGuideProps) {
  const link = { guide: "/docs/guides", reference: "/docs/reference", ...hrefs }
  return (
    <DocsShell page="guide" hrefs={hrefs} {...props}>
      <div className="grid grid-cols-[minmax(0,1fr)] gap-12 px-4 py-10 sm:px-8 xl:grid-cols-[minmax(0,1fr)_14rem]">
        <main className="mx-auto w-full min-w-0 max-w-3xl">
          <Crumbs items={["Get started", "Send your first message"]} />
          <h1 className={cn("mt-5 text-[clamp(2.4rem,5vw,3.8rem)] leading-[1.05] text-balance", docsSerifClass)}>Send your first message</h1>
          <p className="text-muted-foreground mt-4 text-xl text-pretty">Create a queue, put a message on it and take it off again. By the end you will have seen the whole loop that everything else is built on.</p>
          <p className="text-muted-foreground mt-5 flex items-center gap-4 text-sm"><span className="inline-flex items-center gap-1.5"><Clock className="size-4" aria-hidden="true" />7 min read</span><span>Updated Sep 24, 2026</span></p>

          <div className="mt-10">
            <Step n={1} id="before" title="Before you begin">
              <p className="text-pretty">You need a Tern account and a test API key. Test keys only touch test queues, so nothing here can affect production.</p>
              <div className="bg-card flex items-center justify-between gap-3 rounded-xl border p-3 ps-4">
                <div className="min-w-0"><p className="text-muted-foreground text-xs">Your test key</p><p className={cn("truncate text-sm", docsMonoClass)}>tern_test_4fJ2kQ9xN7mB1c</p></div>
                <CopyButton value="tern_test_4fJ2kQ9xN7mB1c" ariaLabel="Copy the test key" variant="outline" size="sm" />
              </div>
              <Callout kind="note" title="Pick a region once">Queues live in one region. Use <code className={docsMonoClass}>eu</code>, <code className={docsMonoClass}>us</code> or <code className={docsMonoClass}>ap</code> and keep it the same in every client.</Callout>
            </Step>
            <Step n={2} id="install" title="Install the SDK">
              <p className="text-pretty">Add the Node package with your package manager. Using Python or Go? Switch the tabs below and every sample on this page follows.</p>
              <InstallTabs storageKey="tern-pm" commands={{ npm: "npm install @tern/sdk", pnpm: "pnpm add @tern/sdk", yarn: "yarn add @tern/sdk", bun: "bun add @tern/sdk" }} />
            </Step>
            <Step n={3} id="create" title="Create a queue">
              <p className="text-pretty">A queue holds messages until a consumer acknowledges them. The visibility timeout is how long a received message stays hidden before Tern offers it to someone else.</p>
              <SnippetTabs storageKey="tern-lang" title="Create a queue" lineNumbers snippets={createSnippets} />
              <Callout kind="tip" title="Names are permanent">A queue cannot be renamed. Pick something you will still like in a year, such as <code className={docsMonoClass}>invoices</code> rather than <code className={docsMonoClass}>test2</code>.</Callout>
            </Step>
            <Step n={4} id="send" title="Send a message">
              <p className="text-pretty">The body can be any JSON up to 256 KB. Pass an idempotency key and a network retry returns the first message instead of adding a second.</p>
              <SnippetTabs storageKey="tern-lang" title="Send a message" lineNumbers snippets={sendSnippets} />
              <CodeBlock filename="response.json" language="json" code={`{\n  "id": "msg_7Hq2Zc",\n  "queue": "invoices",\n  "visibleAt": "2026-09-30T08:14:22Z"\n}`} highlight={[2]} />
            </Step>
            <Step n={5} id="receive" title="Receive and acknowledge" last>
              <p className="text-pretty">Consuming is a loop: wait for a message, do the work, then acknowledge. Acknowledging deletes the message. If your code crashes first, the visibility timeout ends and Tern hands the message to the next consumer.</p>
              <SnippetTabs storageKey="tern-lang" title="Consume messages" lineNumbers snippets={consumeSnippets} />
              <Callout kind="caution" title="Acknowledge only after the work succeeds">Delivery is at least once. A message can arrive twice, so make your handler safe to run twice. The idempotent consumers guide shows how.</Callout>
            </Step>
          </div>

          <section aria-labelledby="next" className="mt-14">
            <h2 id="next" className={cn("scroll-mt-24 text-3xl", docsSerifClass)}>Next steps</h2>
            <ul className="mt-5 grid gap-4 sm:grid-cols-2">
              {[["Delivery guarantees", "What at-least-once really means for your code.", link.guide], ["Dead-letter queues", "Park messages that keep failing and replay them later.", link.guide], ["Idempotent consumers", "Make retries safe.", link.guide], ["API reference", "Every endpoint, with examples.", link.reference]].map(([t, d, h]) => (
                <li key={t}><a href={h} className="bg-card hover:border-primary/50 focus-visible:ring-ring/50 group block h-full rounded-xl border p-5 outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none"><span className="flex items-center justify-between font-semibold">{t}<ArrowRight className="size-4 transition-transform group-hover:translate-x-1 motion-reduce:transition-none rtl:rotate-180 rtl:group-hover:-translate-x-1" aria-hidden="true" /></span><span className="text-muted-foreground mt-1 block text-sm">{d}</span></a></li>
              ))}
            </ul>
          </section>

          <div className="mt-12"><Helpful /></div>

          <nav aria-label="Previous and next page" className="mt-8 grid gap-4 sm:grid-cols-2">
            <a href={link.guide} className="hover:border-primary/50 focus-visible:ring-ring/50 group rounded-xl border p-5 outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none"><span className="text-muted-foreground flex items-center gap-1 text-xs"><ArrowLeft className="size-3.5 rtl:rotate-180" aria-hidden="true" />Previous</span><span className="mt-1 block font-semibold">Core concepts</span></a>
            <a href={link.guide} className="hover:border-primary/50 focus-visible:ring-ring/50 group rounded-xl border p-5 text-end outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none"><span className="text-muted-foreground flex items-center justify-end gap-1 text-xs">Next<ArrowRight className="size-3.5 rtl:rotate-180" aria-hidden="true" /></span><span className="mt-1 block font-semibold">Delivery guarantees</span></a>
          </nav>
        </main>
        <aside className="hidden xl:block" aria-label="On this page"><div className="sticky top-24"><TableOfContents items={toc} title="On this page" offset={96} /></div></aside>
      </div>
    </DocsShell>
  )
}

export { DocsGuide, type DocsGuideProps }
```

### `registry/ballmac/components/templates/docs/docs-reference.tsx`

```tsx
// Ballmac UI: Docs API reference page. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { Search } from "lucide-react"

import { ApiEndpoint } from "@/components/ballmac/api-endpoint"
import { Callout } from "@/components/ballmac/callout"
import { CopyButton } from "@/components/ballmac/copy-button"
import { TableOfContents } from "@/components/ballmac/table-of-contents"
import { endpointGroups } from "@/components/ballmac/templates/docs/docs-data"
import { Crumbs, DocsShell, docsMonoClass, docsSerifClass, type DocsHrefs } from "@/components/ballmac/templates/docs/docs-theme"
import { cn } from "@/lib/utils"

const methods = ["All", "GET", "POST", "DELETE"] as const
const limits = [["Requests", "600 per minute per key"], ["Message size", "256 KB"], ["Batch receive", "50 messages"], ["Long poll", "20 seconds"], ["Idempotency keys", "24 hours"]]
const toc = [
  { id: "auth", title: "Authentication", level: 2 },
  { id: "limits", title: "Limits", level: 2 },
  { id: "queues", title: "Queues", level: 2 },
  { id: "messages", title: "Messages", level: 2 },
]

type DocsReferenceProps = React.ComponentProps<"div"> & { hrefs?: Partial<DocsHrefs> }

/** The API reference: authentication, limits and expandable endpoint cards that filter by method or text. */
function DocsReference({ hrefs, ...props }: DocsReferenceProps) {
  const [method, setMethod] = React.useState<(typeof methods)[number]>("All")
  const [query, setQuery] = React.useState("")
  const q = query.trim().toLowerCase()
  const groups = endpointGroups
    .map((g) => ({ ...g, endpoints: g.endpoints.filter((e) => (method === "All" || e.method === method) && (!q || `${e.summary} ${e.path} ${e.description}`.toLowerCase().includes(q))) }))
    .filter((g) => g.endpoints.length > 0)
  const total = groups.reduce((n, g) => n + g.endpoints.length, 0)
  return (
    <DocsShell page="reference" hrefs={hrefs} {...props}>
      <div className="grid grid-cols-[minmax(0,1fr)] gap-12 px-4 py-10 sm:px-8 xl:grid-cols-[minmax(0,1fr)_14rem]">
        <main className="mx-auto w-full min-w-0 max-w-3xl">
          <Crumbs items={["API reference", "Overview"]} />
          <h1 className={cn("mt-5 text-[clamp(2.4rem,5vw,3.8rem)] leading-[1.05] text-balance", docsSerifClass)}>API reference</h1>
          <p className="text-muted-foreground mt-4 text-xl text-pretty">A small, regular HTTP API. Every endpoint takes and returns JSON, and every SDK method maps to exactly one of them.</p>
          <div className="bg-card mt-6 flex items-center justify-between gap-3 rounded-xl border p-3 ps-4">
            <div className="min-w-0"><p className="text-muted-foreground text-xs">Base URL</p><p className={cn("truncate text-sm", docsMonoClass)}>https://eu.api.tern.dev</p></div>
            <CopyButton value="https://eu.api.tern.dev" ariaLabel="Copy the base URL" variant="outline" size="sm" />
          </div>

          <section aria-labelledby="auth" className="mt-12">
            <h2 id="auth" className={cn("scroll-mt-24 text-3xl", docsSerifClass)}>Authentication</h2>
            <p className="mt-3 text-pretty">Send your key as a bearer token. Test keys start with <code className={docsMonoClass}>tern_test_</code> and live keys with <code className={docsMonoClass}>tern_live_</code>.</p>
            <pre tabIndex={0} role="region" aria-label="Authorization header" className={cn("bg-muted/60 mt-4 overflow-x-auto rounded-xl border p-4 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", docsMonoClass)}>Authorization: Bearer tern_live_••••••••</pre>
            <div className="mt-4"><Callout kind="caution" title="Keep live keys on the server">Never ship a live key in a browser or a mobile app. Create a restricted key that can only send messages if a client must call Tern directly.</Callout></div>
          </section>

          <section aria-labelledby="limits" className="mt-12">
            <h2 id="limits" className={cn("scroll-mt-24 text-3xl", docsSerifClass)}>Limits</h2>
            <dl className="mt-4 divide-y rounded-xl border">
              {limits.map(([k, v]) => <div key={k} className="grid grid-cols-2 gap-4 px-4 py-3 text-sm"><dt className="text-muted-foreground">{k}</dt><dd className={cn("font-medium", docsMonoClass)}>{v}</dd></div>)}
            </dl>
          </section>

          <div className="mt-14 flex flex-wrap items-center gap-3" role="group" aria-label="Filter endpoints">
            <div className="relative min-w-52 flex-1">
              <label htmlFor="dr-q" className="sr-only">Filter endpoints</label>
              <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" />
              <input id="dr-q" type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Filter endpoints" className="bg-card focus-visible:ring-ring/50 h-10 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" />
            </div>
            <div className="flex gap-1.5">
              {methods.map((m) => (
                <button key={m} type="button" aria-pressed={method === m} onClick={() => setMethod(m)} className={cn("hover:bg-accent focus-visible:ring-ring/50 aria-pressed:bg-primary aria-pressed:text-primary-foreground h-10 rounded-lg border px-3 text-xs font-bold outline-none focus-visible:ring-[3px]", docsMonoClass)}>{m}</button>
              ))}
            </div>
          </div>
          <p role="status" className="text-muted-foreground mt-3 text-sm">{total} {total === 1 ? "endpoint" : "endpoints"}</p>

          {groups.map((g) => (
            <section key={g.title} aria-labelledby={g.title.toLowerCase()} className="mt-8">
              <h2 id={g.title.toLowerCase()} className={cn("scroll-mt-24 text-3xl", docsSerifClass)}>{g.title}</h2>
              <div className="mt-5 grid gap-4">
                {g.endpoints.map((e, i) => (
                  <ApiEndpoint key={e.method + e.path} method={e.method} path={e.path} summary={e.summary} description={e.description} baseUrl="https://eu.api.tern.dev" auth="Bearer key" parameters={e.parameters} requestExample={e.requestExample} responses={e.responses} defaultOpen={i === 0 && g.title === groups[0]?.title} />
                ))}
              </div>
            </section>
          ))}
          {total === 0 && <div className="bg-surface mt-8 rounded-2xl border p-10 text-center"><p className={cn("text-2xl", docsSerifClass)}>No endpoint matches “{query}”.</p><p className="text-muted-foreground mt-2 text-sm">Try a shorter word, or clear the method filter.</p><button type="button" onClick={() => { setQuery(""); setMethod("All") }} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 mt-5 h-10 rounded-lg px-5 text-sm font-semibold outline-none focus-visible:ring-[3px]">Clear filters</button></div>}
        </main>
        <aside className="hidden xl:block" aria-label="On this page"><div className="sticky top-24"><TableOfContents items={toc} title="On this page" offset={96} /></div></aside>
      </div>
    </DocsShell>
  )
}

export { DocsReference, type DocsReferenceProps }
```

### `registry/ballmac/components/templates/docs/docs-search.tsx`

```tsx
// Ballmac UI: Docs search page. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { Search } from "lucide-react"

import { searchIndex, type DocsKind } from "@/components/ballmac/templates/docs/docs-data"
import { DocsShell, docsKindStyle, docsMonoClass, docsSerifClass, type DocsHrefs } from "@/components/ballmac/templates/docs/docs-theme"
import { cn } from "@/lib/utils"

const kinds = ["All", "Guide", "API", "Changelog"] as const
const suggestions = ["retry", "idempotency", "dead letter", "visibility", "region"]

function Marked({ text, query }: { text: string; query: string }) {
  const terms = query.trim().split(/\s+/).filter(Boolean).map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
  if (terms.length === 0) return <>{text}</>
  const parts = text.split(new RegExp(`(${terms.join("|")})`, "ig"))
  return <>{parts.map((p, i) => (i % 2 === 1 ? <mark key={i} className="bg-chart-3/35 text-foreground rounded-sm px-0.5">{p}</mark> : <React.Fragment key={i}>{p}</React.Fragment>))}</>
}

type DocsSearchProps = React.ComponentProps<"div"> & { hrefs?: Partial<DocsHrefs>; initialQuery?: string }

/** A full search page: filter by type, matches highlighted in titles and summaries, helpful suggestions when nothing matches. */
function DocsSearch({ hrefs, initialQuery = "message", ...props }: DocsSearchProps) {
  const [query, setQuery] = React.useState(initialQuery)
  const [kind, setKind] = React.useState<(typeof kinds)[number]>("All")
  const link = { guide: "/docs/guides", reference: "/docs/reference", changelog: "/docs/changelog", ...hrefs }
  const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
  const all = searchIndex.filter((e) => terms.every((t) => `${e.title} ${e.summary} ${e.path}`.toLowerCase().includes(t)))
  const results = all.filter((e) => kind === "All" || e.kind === kind)
  const count = (k: DocsKind) => all.filter((e) => e.kind === k).length
  return (
    <DocsShell page="search" hrefs={hrefs} sidebar={false} {...props}>
      <main className="mx-auto max-w-3xl px-4 py-14 sm:px-6 sm:py-20">
        <h1 className={cn("text-[clamp(2.4rem,6vw,4rem)] leading-none", docsSerifClass)}>Search</h1>
        <form role="search" onSubmit={(e) => e.preventDefault()} className="mt-8">
          <label htmlFor="ds-q" className="sr-only">Search the docs</label>
          <div className="bg-card focus-within:ring-ring/50 relative rounded-xl border shadow-sm focus-within:ring-[3px]">
            <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-4 size-5 -translate-y-1/2" aria-hidden="true" />
            <input id="ds-q" type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search guides, endpoints and releases" className="h-14 w-full rounded-xl bg-transparent pe-4 ps-12 text-base outline-none" />
          </div>
        </form>
        <div className="mt-5 flex flex-wrap gap-2" role="group" aria-label="Filter by type">
          {kinds.map((k) => (
            <button key={k} type="button" aria-pressed={kind === k} onClick={() => setKind(k)} className="hover:bg-accent focus-visible:ring-ring/50 aria-pressed:bg-primary aria-pressed:text-primary-foreground inline-flex h-9 items-center gap-2 rounded-full border px-4 text-sm font-semibold outline-none focus-visible:ring-[3px]">
              {k === "API" ? "API reference" : k === "Guide" ? "Guides" : k === "Changelog" ? "Releases" : "Everything"}
              {k !== "All" && <span className={cn("text-xs opacity-100", docsMonoClass)}>{count(k)}</span>}
            </button>
          ))}
        </div>
        <p role="status" className="text-muted-foreground mt-6 text-sm">{results.length} {results.length === 1 ? "result" : "results"}{query.trim() ? <> for “{query.trim()}”</> : null}</p>
        {results.length > 0 ? (
          <ul className="mt-2 divide-y">
            {results.map((e) => (
              <li key={e.title}>
                <a href={link[e.page]} className="hover:bg-accent focus-visible:ring-ring/50 -mx-3 block rounded-xl px-3 py-5 outline-none focus-visible:ring-[3px]">
                  <span className="flex items-center gap-2 text-xs">
                    <span className={cn("rounded-md px-2 py-0.5 font-bold", docsKindStyle[e.kind])}>{e.kind === "API" ? "API" : e.kind}</span>
                    <span className={cn("text-muted-foreground truncate", e.kind === "API" && docsMonoClass)}>{e.path}</span>
                  </span>
                  <span className={cn("mt-2 block text-2xl", docsSerifClass)}><Marked text={e.title} query={query} /></span>
                  <span className="text-muted-foreground mt-1 block text-pretty"><Marked text={e.summary} query={query} /></span>
                </a>
              </li>
            ))}
          </ul>
        ) : (
          <div className="bg-surface mt-4 rounded-2xl border p-10 text-center">
            <p className={cn("text-3xl", docsSerifClass)}>Nothing for “{query.trim()}”.</p>
            <p className="text-muted-foreground mt-2">Check the spelling, try fewer words or start from one of these.</p>
            <div className="mt-6 flex flex-wrap justify-center gap-2">{suggestions.map((s) => <button key={s} type="button" onClick={() => { setQuery(s); setKind("All") }} className="bg-card hover:bg-accent focus-visible:ring-ring/50 h-9 rounded-full border px-4 text-sm font-semibold outline-none focus-visible:ring-[3px]">{s}</button>)}</div>
          </div>
        )}
      </main>
    </DocsShell>
  )
}

export { DocsSearch, type DocsSearchProps }
```

### `registry/ballmac/components/templates/docs/docs-changelog.tsx`

```tsx
// Ballmac UI: Docs changelog page. https://ui.ballmac.com/templates/template-docs
"use client"

import * as React from "react"
import { Rss } from "lucide-react"

import { releases, type Release } from "@/components/ballmac/templates/docs/docs-data"
import { DocsShell, docsMonoClass, docsSerifClass, type DocsHrefs } from "@/components/ballmac/templates/docs/docs-theme"
import { cn } from "@/lib/utils"

const tags = ["All", "Added", "Improved", "Fixed", "Breaking"] as const
const tagStyle: Record<Release["tag"], string> = { Added: "bg-chart-4/15", Improved: "bg-chart-2/15", Fixed: "bg-chart-3/20", Breaking: "bg-destructive/15" }
/** Renders `code` spans written with backticks. */
function Ticks({ text }: { text: string }) {
  return <>{text.split("`").map((part, i) => (i % 2 === 1 ? <code key={i} className={cn("bg-muted rounded px-1 py-0.5 text-[0.85em]", docsMonoClass)}>{part}</code> : <React.Fragment key={i}>{part}</React.Fragment>))}</>
}
const fmt = (d: string) => new Date(d + "T00:00:00Z").toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" })

type DocsChangelogProps = React.ComponentProps<"div"> & { hrefs?: Partial<DocsHrefs> }

/** The changelog: a version rail, tag filters and each release with its upgrade notes. */
function DocsChangelog({ hrefs, ...props }: DocsChangelogProps) {
  const [tag, setTag] = React.useState<(typeof tags)[number]>("All")
  const list = releases.filter((r) => tag === "All" || r.tag === tag)
  return (
    <DocsShell page="changelog" hrefs={hrefs} {...props}>
      <div className="grid grid-cols-[minmax(0,1fr)] gap-12 px-4 py-10 sm:px-8 xl:grid-cols-[minmax(0,1fr)_14rem]">
        <main className="mx-auto w-full min-w-0 max-w-3xl">
          <h1 className={cn("text-[clamp(2.4rem,5vw,3.8rem)] leading-[1.05]", docsSerifClass)}>Changelog</h1>
          <p className="text-muted-foreground mt-4 text-xl text-pretty">What shipped, what changed and what to do about it. Breaking changes always get a major version and a month of notice.</p>
          <div className="mt-6 flex flex-wrap items-center gap-2" role="group" aria-label="Filter releases">
            {tags.map((t) => <button key={t} type="button" aria-pressed={tag === t} onClick={() => setTag(t)} className="hover:bg-accent focus-visible:ring-ring/50 aria-pressed:bg-primary aria-pressed:text-primary-foreground h-9 rounded-full border px-4 text-sm font-semibold outline-none focus-visible:ring-[3px]">{t}</button>)}
            <a href="/docs/changelog.xml" className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 ms-auto inline-flex items-center gap-1.5 rounded-md text-sm font-medium outline-none focus-visible:ring-[3px]"><Rss className="size-4" aria-hidden="true" />RSS</a>
          </div>
          <p role="status" className="text-muted-foreground mt-4 text-sm">{list.length} {list.length === 1 ? "release" : "releases"}</p>
          <ol className="mt-4">
            {list.map((r, i) => (
              <li key={r.version} id={`v${r.version}`} className="relative grid scroll-mt-24 grid-cols-[1.5rem_minmax(0,1fr)] gap-x-4">
                <div className="flex flex-col items-center"><span aria-hidden="true" className={cn("mt-2 size-3 rounded-full border-2", r.tag === "Breaking" ? "border-destructive bg-destructive" : "border-primary bg-background")} />{i < list.length - 1 && <span aria-hidden="true" className="bg-border mt-1 w-px flex-1" />}</div>
                <article aria-labelledby={`r-${r.version}`} className="pb-12">
                  <div className="flex flex-wrap items-center gap-3 text-sm">
                    <span className={cn("bg-secondary rounded-md px-2 py-0.5 text-xs font-bold", docsMonoClass)}>{r.version}</span>
                    <span className={cn("rounded-md px-2 py-0.5 text-xs font-bold", tagStyle[r.tag])}>{r.tag}</span>
                    <time dateTime={r.date} className="text-muted-foreground">{fmt(r.date)}</time>
                  </div>
                  <h2 id={`r-${r.version}`} className={cn("mt-3 text-3xl", docsSerifClass)}>{r.title}</h2>
                  <p className="mt-2 text-pretty"><Ticks text={r.body} /></p>
                  <ul className="mt-4 grid gap-2">{r.points.map((p) => <li key={p} className="text-muted-foreground flex gap-3 text-sm"><span className="bg-border mt-2 size-1.5 shrink-0 rounded-full" aria-hidden="true" /><span className="text-pretty"><Ticks text={p} /></span></li>)}</ul>
                </article>
              </li>
            ))}
          </ol>
          {list.length === 0 && <p className="text-muted-foreground py-10 text-center">No releases with this tag yet.</p>}
        </main>
        <aside className="hidden xl:block" aria-label="Versions"><div className="sticky top-24"><h2 className="text-xs font-bold tracking-[0.12em] uppercase">Versions</h2><ul className="mt-3 grid gap-1 border-s text-sm">{releases.map((r) => <li key={r.version}><a href={`#v${r.version}`} className={cn("text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -ms-px block rounded-e-md border-s-2 border-transparent px-3 py-1 outline-none hover:border-current focus-visible:ring-[3px]", docsMonoClass)}>{r.version}</a></li>)}</ul></div></aside>
      </div>
    </DocsShell>
  )
}

export { DocsChangelog, type DocsChangelogProps }
```

### `registry/ballmac/app/docs/page.tsx`

```tsx
// Ballmac UI: Docs template route. https://ui.ballmac.com/templates/template-docs
import type { Metadata } from "next"

import { DocsHome } from "@/components/ballmac/templates/docs/docs-home"

export const metadata: Metadata = {
  title: "Tern docs: queues that never lose a message",
  description: "Guides, API reference and changelog for Tern.",
}

export default function Page() {
  return <DocsHome />
}
```

### `registry/ballmac/app/docs/guides/page.tsx`

```tsx
// Ballmac UI: Docs template route. https://ui.ballmac.com/templates/template-docs
import type { Metadata } from "next"

import { DocsGuide } from "@/components/ballmac/templates/docs/docs-guide"

export const metadata: Metadata = {
  title: "Send your first message · Tern docs",
  description: "Create a queue, send a message and acknowledge it in about seven minutes.",
}

export default function Page() {
  return <DocsGuide />
}
```

### `registry/ballmac/app/docs/reference/page.tsx`

```tsx
// Ballmac UI: Docs template route. https://ui.ballmac.com/templates/template-docs
import type { Metadata } from "next"

import { DocsReference } from "@/components/ballmac/templates/docs/docs-reference"

export const metadata: Metadata = {
  title: "API reference · Tern docs",
  description: "Every endpoint, parameter and error code.",
}

export default function Page() {
  return <DocsReference />
}
```

### `registry/ballmac/app/docs/search/page.tsx`

```tsx
// Ballmac UI: Docs template route. https://ui.ballmac.com/templates/template-docs
import type { Metadata } from "next"

import { DocsSearch } from "@/components/ballmac/templates/docs/docs-search"

export const metadata: Metadata = {
  title: "Search · Tern docs",
  description: "Search guides, endpoints and releases.",
}

export default function Page() {
  return <DocsSearch />
}
```

### `registry/ballmac/app/docs/changelog/page.tsx`

```tsx
// Ballmac UI: Docs template route. https://ui.ballmac.com/templates/template-docs
import type { Metadata } from "next"

import { DocsChangelog } from "@/components/ballmac/templates/docs/docs-changelog"

export const metadata: Metadata = {
  title: "Changelog · Tern docs",
  description: "What shipped, what changed and what to do about it.",
}

export default function Page() {
  return <DocsChangelog />
}
```
