# Search in file

> Live search marking every match, with a counter that reads the block through useCodeBlockConfig.

A shadcn/ui block from the [@reui](https://reui.io) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/reui/c-code-block-13).

- Page, with a live preview: https://shadcn.exchange/r/reui/c-code-block-13
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-code-block-13-light-11f3bc9e8a99ab1f.png
- Registry item: https://reui.io/r/new-york-v4/c-code-block-13.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://reui.io/r/new-york-v4/c-code-block-13.json
```

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

## Dependencies

- Registry items: `@reui/badge`, `button`, `button-group`, `@reui/code-block`, `input`, `scroll-area`

## Files

### `c-code-block-13.tsx`

```tsx
"use client"

import { useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockTitle,
  useCodeBlockConfig,
} from "@/components/reui/code-block/code-block"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"

const code = `export async function createInvite(workspaceId: string, email: string) {
  const workspace = await db.workspace.findUnique({ where: { id: workspaceId } })
  if (!workspace) throw new WorkspaceNotFoundError(workspaceId)

  const seats = await countBillableSeats(workspaceId)
  if (seats.used >= seats.total) {
    throw new SeatLimitError(workspaceId, seats.total)
  }

  const invite = await db.invite.create({
    data: { workspaceId, email, status: "pending" },
  })

  await mail.send({ to: email, template: "workspace-invite" })
  return invite
}`

/**
 * The counter takes no `code` prop: `useCodeBlockConfig` hands it the block's
 * own source, so a control composed into the header cannot drift from what the
 * block actually renders.
 *
 * Counting is case SENSITIVE on purpose. `highlightedWords` marks exact
 * matches, so a case-insensitive count reports 11 where the block paints 10
 * and the header quietly contradicts the code under it.
 */
function MatchCount({ query }: { query: string }) {
  const { code: source } = useCodeBlockConfig("MatchCount")

  const matches = useMemo(() => {
    const needle = query.trim()
    if (needle.length < 2) return null
    return source.split(needle).length - 1
  }, [source, query])

  if (matches === null) return null

  return (
    <Badge variant={matches ? "primary-light" : "secondary"}>
      {matches} {matches === 1 ? "match" : "matches"}
    </Badge>
  )
}

export function Pattern() {
  const [query, setQuery] = useState("workspace")

  /* `highlightedWords` is a spec, not a regex: a query under two characters
     would mark almost every line, so it stays empty until the search is worth
     rendering. */
  const words = useMemo(
    () => (query.trim().length < 2 ? undefined : [query.trim()]),
    [query]
  )

  return (
    <CodeBlock
      code={code}
      language="typescript"
      showLineNumbers
      highlightedWords={words}
      className="w-full max-w-2xl"
    >
      <CodeBlockHeader className="gap-2">
        <CodeBlockTitle>invites.ts</CodeBlockTitle>
        <div className="ml-auto flex items-center gap-2">
          <MatchCount query={query} />
          {/* The group joins the field to its clear button into one control,
                so the header keeps a single bordered shape rather than two. */}
          <ButtonGroup>
            <Input
              type="search"
              value={query}
              aria-label="Search in file"
              placeholder="Search"
              onChange={(event) => setQuery(event.target.value)}
              className="w-40 text-xs"
            />
            <Button
              variant="outline"
              size="icon"
              aria-label="Clear search"
              disabled={!query}
              onClick={() => setQuery("")}
            >
              <IconPlaceholder
                lucide="XIcon"
                tabler="IconX"
                hugeicons="Cancel01Icon"
                phosphor="XIcon"
                remixicon="RiCloseLine"
              />
            </Button>
          </ButtonGroup>
        </div>
      </CodeBlockHeader>

      {/* Pinned over the code: the header is already carrying the search
            control, so the copy affordance moves onto the surface itself. */}
      <CodeBlockCopyButton
        variant="outline"
        size="icon-xs"
        className="bg-card hover:bg-muted"
      />
      <ScrollArea className="rounded-[inherit] **:data-[slot=scroll-area-viewport]:max-h-72">
        <CodeBlockContent />
        <ScrollBar orientation="horizontal" />
      </ScrollArea>
    </CodeBlock>
  )
}
```
