{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "highlight",
  "type": "registry:ui",
  "title": "Highlight",
  "description": "Semantic marker that wraps the words matching a search query in a native <mark>, the HTML element for text relevant to the user's current activity. Rendered entirely on the server, styled with theme tokens, zero client JS — drop it inside the fragment htmx swaps into a results list.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/highlight.tsx",
      "type": "registry:ui",
      "target": "components/ui/highlight.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { Child } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// Highlight — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// Semantic marker for text that is relevant to the user's current activity —\n// the canonical case is the words that matched a search query. It is a thin,\n// server-rendered wrapper around the native <mark> element; the server splits\n// a string on the query terms and wraps each match in a styled <mark>.\n//\n// Source of truth:\n//   - <mark> semantics + the \"search results\" use case + the screen-reader\n//     announcement note:\n//     repos/mdn/files/en-us/web/html/reference/elements/mark/index.md\n//     (\"`<mark>` indicates a portion of the document's content which is likely\n//      to be relevant to the user's current activity … the words that matched\n//      a search operation.\" MDN also notes that `<mark>` is NOT announced by\n//      default — which is correct here: a highlighted search match is a visual\n//      affordance, not extra content to read out. Do not abuse the\n//      ::before/::after announcement trick on a results list.)\n//   - Don't use <mark> for syntax highlighting — use <span> (MDN, same file).\n//     This component is strictly for relevance, never decoration.\n//\n// htmx: nothing of its own. The component just produces the marked-up HTML;\n// the server renders it inside whatever fragment htmx swaps in (e.g. the\n// <tr> rows returned to an Active Search `hx-target`). It forwards hx-*/data-*/\n// aria-* via {...rest} so a single highlighted term can also be a swap hook.\n// Verified there is no <mark>-specific htmx attribute:\n//   repos/htmx/www/reference.md\n//\n// JS budget: none. Pure SSR + one CSS rule's worth of utility classes. The\n// native <mark> default is a yellow background; we reset it to theme tokens so\n// it reads on brand in light and dark and never clashes with selection colours.\n//\n// Accessibility:\n//   - Keep highlighting to genuine matches. WCAG 1.4.1 (Use of Color): the\n//     match must not rely on the tint alone to be perceivable — <mark> is a\n//     real semantic element, and the bold weight + rounded chip give a\n//     non-colour cue, so a match survives in a high-contrast / forced-colours\n//     theme.\n//   - Case-insensitive matching by default; the ORIGINAL casing of the source\n//     text is preserved in the output (we slice the source, never the query).\n\n// Reset the UA yellow default and paint with theme tokens so a match reads on\n// brand in both schemes. bg-primary/15 is a soft tint of the brand colour;\n// text-foreground keeps body contrast; the rounded chip + font-medium are the\n// non-colour cue (WCAG 1.4.1). box-decoration-clone keeps the chip intact when\n// a match wraps across lines.\nconst markBase =\n  \"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\"\n\nexport function highlightClasses(opts?: { class?: ClassValue }): string {\n  return cn(markBase, opts?.class)\n}\n\n// Escape a user-supplied query for safe use inside a RegExp.\nfunction escapeRegExp(s: string): string {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\n// Split `text` on every occurrence of `query` (case-insensitive, whole-string\n// or per-word) and return an alternating list of plain strings and { match }\n// segments. The source casing is preserved — we only ever slice `text`.\nexport type HighlightSegment = { text: string; match: boolean }\n\nexport function splitMatches(\n  text: string,\n  query: string | undefined,\n  opts?: { words?: boolean; caseSensitive?: boolean },\n): HighlightSegment[] {\n  const q = (query ?? \"\").trim()\n  if (q.length === 0) return [{ text, match: false }]\n\n  // words:true highlights each whitespace-separated term independently\n  // (multi-term search). Otherwise the whole query is one phrase.\n  const terms = opts?.words ? q.split(/\\s+/).filter(Boolean) : [q]\n  if (terms.length === 0) return [{ text, match: false }]\n\n  const flags = opts?.caseSensitive ? \"g\" : \"gi\"\n  const re = new RegExp(`(${terms.map(escapeRegExp).join(\"|\")})`, flags)\n\n  const segments: HighlightSegment[] = []\n  let last = 0\n  for (const m of text.matchAll(re)) {\n    const start = m.index\n    if (start > last) segments.push({ text: text.slice(last, start), match: false })\n    segments.push({ text: m[0], match: true })\n    last = start + m[0].length\n  }\n  if (last < text.length) segments.push({ text: text.slice(last), match: false })\n  return segments.length > 0 ? segments : [{ text, match: false }]\n}\n\ntype HighlightProps = {\n  // The source text to scan. Its original casing is preserved in the output.\n  text?: string\n  // The query to mark inside `text`. Empty/undefined renders `text` verbatim.\n  query?: string\n  // Highlight each whitespace-separated term in `query` independently.\n  words?: boolean\n  // Match case exactly (default: case-insensitive).\n  caseSensitive?: boolean\n  // Alternative \"single term\" mode: wrap the children verbatim in one <mark>.\n  // Use when the server already knows the exact run to mark (no scanning).\n  children?: Child\n  class?: ClassValue\n  // hx-* / data-* / aria-* ride onto the <mark> (or the wrapper in scan mode).\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n  [key: `aria-${string}`]: any\n}\n\nexport function Highlight(props: HighlightProps) {\n  const {\n    text,\n    query,\n    words,\n    caseSensitive,\n    children,\n    class: className,\n    ...rest\n  } = props\n\n  const classes = highlightClasses({ class: className })\n\n  // Single-term mode: the caller hands us the exact run to mark.\n  if (children !== undefined) {\n    return (\n      <mark data-slot=\"highlight\" class={classes} {...rest}>\n        {children}\n      </mark>\n    )\n  }\n\n  // Scan mode: split the source on the query and wrap each match. The root\n  // carries data-slot=\"highlight\" so the whole rendered run is one styling /\n  // testing hook even though only the matched bits are <mark>.\n  const segments = splitMatches(text ?? \"\", query, { words, caseSensitive })\n  return (\n    <span data-slot=\"highlight\" {...rest}>\n      {segments.map((seg) =>\n        seg.match ? <mark class={classes}>{seg.text}</mark> : seg.text,\n      )}\n    </span>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/highlight.html",
      "type": "registry:file",
      "target": "templates/components/highlight.html",
      "content": "{# Highlight macro — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/highlight.tsx. Wraps query matches in a styled native\n   <mark>; the marker is the semantic element for \"text relevant to the user's\n   current activity\" — the words that matched a search.\n     repos/mdn/files/en-us/web/html/reference/elements/mark/index.md\n   The UA yellow default is reset to theme tokens (bg-primary/15 + text-foreground)\n   so it reads on brand in light/dark. No htmx attribute is <mark>-specific\n   (repos/htmx/www/reference.md); hx-*/data-*/aria-* ride through **attrs.\n\n   Two modes, same as the .tsx:\n     1. highlight(text, query)  — scan `text` and mark each match.\n     2. mark(...) as a {% call %} block — wrap a body the server already\n        sliced (single-term mode).\n\n   Usage:\n     {% from \"components/highlight.html\" import highlight, mark %}\n     {{ highlight(\"Several species of salamander\", query=\"salamander\") }}\n     {% call mark() %}Imperial{% endcall %} #}\n\n{%- set _mark_class = \"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\" -%}\n\n{# Single-term mode: wrap the body verbatim in one <mark>. #}\n{% macro mark(extra_class=\"\", **attrs) %}\n<mark data-slot=\"highlight\" class=\"{{ _mark_class }} {{ extra_class }}\"\n  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n>{{ caller() }}</mark>\n{%- endmacro %}\n\n{# Scan mode: split `text` on `query` (case-insensitive by default) and wrap\n   each match. Source casing is preserved — only `text` is sliced. `words=true`\n   marks each whitespace-separated term independently. #}\n{% macro highlight(text, query=none, words=false, case_sensitive=false, extra_class=\"\", **attrs) %}\n<span data-slot=\"highlight\"\n  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n>\n{%- set q = (query | default(\"\", true)) | trim -%}\n{%- if q | length == 0 -%}\n  {{ text }}\n{%- else -%}\n  {%- set terms = (q.split() if words else [q]) -%}\n  {%- set hay = text if case_sensitive else text | lower -%}\n  {%- set ns = namespace(i=0) -%}\n  {%- for _ in text -%}\n    {%- if ns.i < (text | length) -%}\n      {# find the earliest matching term at or after ns.i #}\n      {%- set best = namespace(at=-1, len=0) -%}\n      {%- for t in terms -%}\n        {%- set needle = t if case_sensitive else t | lower -%}\n        {%- set pos = hay.find(needle, ns.i) -%}\n        {%- if pos != -1 and (best.at == -1 or pos < best.at) -%}\n          {%- set best.at = pos -%}\n          {%- set best.len = needle | length -%}\n        {%- endif -%}\n      {%- endfor -%}\n      {%- if best.at == -1 -%}\n        {{- text[ns.i:] -}}\n        {%- set ns.i = text | length -%}\n      {%- else -%}\n        {{- text[ns.i:best.at] -}}\n        <mark class=\"{{ _mark_class }} {{ extra_class }}\">{{ text[best.at:best.at + best.len] }}</mark>\n        {%- set ns.i = best.at + best.len -%}\n      {%- endif -%}\n    {%- endif -%}\n  {%- endfor -%}\n{%- endif -%}\n</span>\n{%- endmacro %}\n"
    },
    {
      "path": "registry/go-templates/highlight.tmpl",
      "type": "registry:file",
      "target": "components/highlight.tmpl",
      "content": "{{/*\n  Highlight template — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/highlight.tsx. Wraps query matches in a styled native\n  <mark> — the semantic element for \"text relevant to the user's current\n  activity\", i.e. the words that matched a search.\n    repos/mdn/files/en-us/web/html/reference/elements/mark/index.md\n  The UA yellow default is reset to theme tokens (bg-primary/15 + text-foreground)\n  so a match reads on brand in light/dark. No htmx attribute is <mark>-specific\n  (repos/htmx/www/reference.md); pass hx-*/data-* through .Attrs.\n\n  Go has no inline regex in templates, so the SCAN happens in Go and you pass\n  the result as an ordered []Segment. This keeps the same semantic HTML as the\n  other flavours (alternating text + <mark> runs), with the split logic where\n  Go wants it.\n\n      type Segment struct {\n          Text  string\n          Match bool\n      }\n      type HighlightArgs struct {\n          Segments  []Segment         // ordered runs from your splitMatches in Go\n          Attrs     map[string]string // hx-*/data-*/aria-* on the wrapper\n          ExtraClass string\n      }\n      // Single-term mode:\n      type MarkArgs struct {\n          Body       string            // exact run to mark (already sliced)\n          Attrs      map[string]string\n          ExtraClass string\n      }\n\n  Usage:\n    {{template \"highlight\" (dict \"Segments\" $segs)}}\n    {{template \"mark\" (dict \"Body\" \"Imperial\")}}\n*/}}\n\n{{- define \"_highlight_mark_class\" -}}rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]{{- end -}}\n\n{{/* Single-term mode: wrap a server-sliced run in one <mark>. */}}\n{{define \"mark\"}}\n{{- $extra := or .ExtraClass \"\" -}}\n<mark data-slot=\"highlight\" class=\"{{template \"_highlight_mark_class\"}} {{$extra}}\"\n  {{- range $k, $v := .Attrs}} {{$k}}=\"{{$v}}\"{{end -}}\n>{{.Body}}</mark>\n{{end}}\n\n{{/* Scan mode: range over the pre-split segments; mark the matched ones. */}}\n{{define \"highlight\"}}\n{{- $extra := or .ExtraClass \"\" -}}\n<span data-slot=\"highlight\"\n  {{- range $k, $v := .Attrs}} {{$k}}=\"{{$v}}\"{{end -}}\n>\n{{- range .Segments -}}\n  {{- if .Match -}}\n    <mark class=\"{{template \"_highlight_mark_class\"}} {{$extra}}\">{{.Text}}</mark>\n  {{- else -}}\n    {{- .Text -}}\n  {{- end -}}\n{{- end -}}\n</span>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/highlight.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/highlight.ex",
      "content": "defmodule ShadcnHtmx.Components.Highlight do\n  @moduledoc \"\"\"\n  Highlight — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/highlight.tsx. Wraps query matches in a styled native\n  `<mark>` — the semantic element for \"text relevant to the user's current\n  activity\", i.e. the words that matched a search.\n\n    * repos/mdn/files/en-us/web/html/reference/elements/mark/index.md\n      (`<mark>` marks \"a portion of the document's content which is likely to be\n      relevant to the user's current activity … the words that matched a search\n      operation.\" Don't use it for syntax highlighting — that's a `<span>`.)\n\n  The UA yellow `<mark>` default is reset to theme tokens (`bg-primary/15` +\n  `text-foreground`) so a match reads on brand in light and dark.\n\n  htmx: nothing of its own. The component renders the marked-up HTML the server\n  swaps into an `hx-target`; no `<mark>`-specific attribute exists\n  (repos/htmx/www/reference.md). `hx-*/data-*/aria-*` ride through `@rest`.\n\n  Two modes:\n\n    * `<.highlight text=\"…\" query=\"…\" />` — scan `text` and mark each match.\n      Source casing is preserved; the query is escaped before matching.\n    * `<.mark>Imperial</.mark>` — wrap a body the server already sliced.\n\n  ## Examples\n\n      <.highlight text=\"Several species of salamander\" query=\"salamander\" />\n      <.mark>Imperial</.mark>\n  \"\"\"\n\n  use Phoenix.Component\n\n  @mark_class \"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\"\n\n  # Single-term mode: wrap a server-sliced run in one <mark>.\n  attr :class, :string, default: nil\n  attr :rest, :global, include: ~w(hx-get hx-post hx-target hx-swap hx-trigger)\n  slot :inner_block, required: true\n\n  def mark(assigns) do\n    assigns = assign(assigns, :mark_class, @mark_class)\n\n    ~H\"\"\"\n    <mark data-slot=\"highlight\" class={[@mark_class, @class]} {@rest}>\n      {render_slot(@inner_block)}\n    </mark>\n    \"\"\"\n  end\n\n  # Scan mode: split `text` on `query` and mark each match.\n  attr :text, :string, default: \"\"\n  attr :query, :string, default: nil\n  attr :words, :boolean, default: false\n  attr :case_sensitive, :boolean, default: false\n  attr :class, :string, default: nil\n  attr :rest, :global, include: ~w(hx-get hx-post hx-target hx-swap hx-trigger)\n\n  def highlight(assigns) do\n    assigns =\n      assigns\n      |> assign(:mark_class, @mark_class)\n      |> assign(\n        :segments,\n        split_matches(assigns.text, assigns.query,\n          words: assigns.words,\n          case_sensitive: assigns.case_sensitive\n        )\n      )\n\n    ~H\"\"\"\n    <span data-slot=\"highlight\" {@rest}><%= for seg <- @segments do %><%= if seg.match do %><mark class={[@mark_class, @class]}>{seg.text}</mark><% else %>{seg.text}<% end %><% end %></span>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Split `text` on every occurrence of `query` (case-insensitive by default).\n  Returns an ordered list of `%{text: binary, match: boolean}` runs; the source\n  casing is preserved. `words: true` marks each whitespace term independently.\n  \"\"\"\n  def split_matches(text, query, opts \\\\ []) do\n    q = String.trim(query || \"\")\n\n    if q == \"\" do\n      [%{text: text, match: false}]\n    else\n      terms = if opts[:words], do: String.split(q, ~r/\\s+/, trim: true), else: [q]\n\n      flags = if opts[:case_sensitive], do: \"\", else: \"i\"\n      pattern = terms |> Enum.map(&Regex.escape/1) |> Enum.join(\"|\")\n      re = Regex.compile!(\"(#{pattern})\", flags)\n\n      Regex.split(re, text, include_captures: true, trim: true)\n      |> Enum.map(fn part -> %{text: part, match: Regex.match?(re, part)} end)\n      |> case do\n        [] -> [%{text: text, match: false}]\n        segs -> segs\n      end\n    end\n  end\nend\n"
    },
    {
      "path": "registry/html/highlight.html",
      "type": "registry:file",
      "target": "snippets/highlight.html",
      "content": "<!--\n  shadcn-htmx — raw Highlight markup.\n\n  Mirrors registry/ui/highlight.tsx. The server wraps the words that matched a\n  search query in a native <mark>; <mark> is the semantic element for \"text\n  relevant to the user's current activity\".\n    repos/mdn/files/en-us/web/html/reference/elements/mark/index.md\n\n  The browser's default <mark> is a yellow background; the classes below reset\n  it to theme tokens so a match reads on brand in light and dark:\n\n    rounded-sm bg-primary/15 px-0.5 font-medium text-foreground\n    [box-decoration-break:clone]\n\n  No script and no <mark>-specific htmx attribute (repos/htmx/www/reference.md):\n  the highlighting is produced server-side and rendered into whatever fragment\n  htmx swaps in. Relies only on theme tokens.\n-->\n\n<!-- Scan mode: the wrapper carries data-slot=\"highlight\"; only the matched\n     runs are <mark>. Here the query was \"salamander\". -->\n<p>\n  Several species of\n  <span data-slot=\"highlight\">Several species of <mark class=\"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\">salamander</mark> inhabit the temperate rainforest.</span>\n</p>\n\n<!-- Inside an htmx-swapped search result row -->\n<li>\n  <span data-slot=\"highlight\">Evading the dreaded <mark class=\"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\">Imperial</mark> Starfleet…</span>\n</li>\n\n<!-- Single-term mode: the server already sliced the exact run, so it's a bare\n     <mark> with data-slot on it directly. -->\n<p>\n  Evading the dreaded\n  <mark data-slot=\"highlight\" class=\"rounded-sm bg-primary/15 px-0.5 font-medium text-foreground [box-decoration-break:clone]\">Imperial</mark>\n  Starfleet.\n</p>\n"
    }
  ]
}
