{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "match-highlight",
  "title": "Match Highlight",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Text with the words of a search marked, as search results show them: real mark elements in a theme tint that stays readable in light and dark, plus helpers that split a query and test whether text matches it.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/match-highlight.tsx",
      "content": "// Ballmac UI: Match Highlight. https://ui.ballmac.com/components/match-highlight\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\n/** The words of a search, lowercased, without blanks or repeats. */\nfunction queryTokens(query: string | string[]): string[] {\n  const words = Array.isArray(query) ? query : query.split(/\\s+/)\n  return [...new Set(words.map((w) => w.trim().toLowerCase()).filter(Boolean))]\n}\n\n/** Whether every word of the search occurs in the text, ignoring case. An empty search matches everything. */\nfunction matchesAll(text: string, query: string | string[]) {\n  const lower = text.toLowerCase()\n  return queryTokens(query).every((t) => lower.includes(t))\n}\n\n/** How many times any word of the search occurs in the text, ignoring case. */\nfunction countMatches(text: string, query: string | string[]) {\n  const tokens = queryTokens(query)\n  if (!tokens.length) return 0\n  return (text.match(new RegExp(tokens.map(escapeRegExp).join(\"|\"), \"gi\")) ?? []).length\n}\n\ntype MatchHighlightProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** The text to show. */\n  text: string\n  /** What was searched for: a string (split into words) or a list of words. Every occurrence of any word is marked. */\n  query?: string | string[]\n}\n\n/**\n * Text with the words of a search marked, as search results show them. It uses the real <mark> element, so screen readers can\n * find the matches, and a tint of a theme color that keeps the text readable in light and dark. Runs on the server.\n */\nfunction MatchHighlight({ text, query = \"\", className, ...props }: MatchHighlightProps) {\n  const tokens = queryTokens(query)\n  if (!tokens.length) return <span data-slot=\"match-highlight\" className={className} {...props}>{text}</span>\n  const parts = text.split(new RegExp(`(${tokens.map(escapeRegExp).join(\"|\")})`, \"gi\"))\n  return (\n    <span data-slot=\"match-highlight\" className={className} {...props}>\n      {parts.map((part, i) =>\n        i % 2 ? (\n          <mark key={i} className={cn(\"bg-chart-3/35 text-foreground rounded-sm\")}>\n            {part}\n          </mark>\n        ) : (\n          part\n        )\n      )}\n    </span>\n  )\n}\n\nexport { MatchHighlight, queryTokens, matchesAll, countMatches, type MatchHighlightProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/match-highlight.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "search",
      "highlight",
      "mark",
      "results",
      "query",
      "autocomplete",
      "snippet"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "<MatchHighlight text={result.title} query={query} /> marks every occurrence of every word of the query, ignoring case. queryTokens(query) gives the words, matchesAll(text, query) tests that all of them occur, countMatches(text, query) counts occurrences for ranking. No \"use client\": it works in server components.",
      "whenToUse": [
        "Search results, suggestions and autocomplete rows",
        "Showing why an item matched a filter"
      ],
      "whenNotToUse": [
        "Syntax highlighting of code (use code-block)",
        "A hand-drawn emphasis mark on a phrase (use highlighter)"
      ],
      "composesWith": [
        "search-field",
        "command",
        "help-1",
        "search-1"
      ],
      "a11y": [
        {
          "keys": "None",
          "action": "Uses <mark>, which assistive technology can announce; the tint is chosen so the text keeps its contrast."
        }
      ],
      "customization": [
        "className sets the look of the wrapper; to change the tint, copy the file and edit the mark's classes",
        "query can be a string or a list of words, so you can pass your own tokens"
      ]
    },
    "statesNone": "A display of text: nothing to load, submit or fail.",
    "examples": [
      "match-highlight-demo",
      "match-highlight-live"
    ],
    "url": "https://ui.ballmac.com/components/match-highlight"
  },
  "categories": [
    "text"
  ],
  "type": "registry:ui"
}