{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "highlight",
  "title": "Highlight",
  "type": "registry:ui",
  "description": "Highlights matching substrings in text for search results. Supports string or regex queries, case-sensitive and whole-word matching, custom highlight tag, and a max-highlight cap.",
  "categories": [
    "display",
    "utility"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/highlight/highlight.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\ninterface Segment {\n  text: string\n  match: boolean\n}\n\nfunction buildPattern(query: string | RegExp, caseSensitive: boolean, wholeWord: boolean): RegExp | null {\n  if (query instanceof RegExp) {\n    const flags = query.flags.includes('g') ? query.flags : query.flags + 'g'\n    return new RegExp(query.source, flags)\n  }\n  if (!query) return null\n  const escaped = String(query).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n  const body = wholeWord ? `\\\\b${escaped}\\\\b` : escaped\n  const flags = caseSensitive ? 'g' : 'gi'\n  return new RegExp(body, flags)\n}\n\n/**\n * <uip-highlight> — highlights matching substrings within text.\n */\nexport class UipHighlight extends LitElement {\n  static styles = [tailwind, css`:host { display: inline; }`]\n\n  static properties = {\n    text: { reflect: true },\n    query: {},\n    highlightTag: { attribute: 'highlight-tag', reflect: true },\n    highlightClass: { attribute: 'highlight-class', reflect: true },\n    caseSensitive: { type: Boolean, attribute: 'case-sensitive', reflect: true },\n    wholeWord: { type: Boolean, attribute: 'whole-word', reflect: true },\n    maxHighlights: { type: Number, attribute: 'max-highlights', reflect: true },\n  }\n\n  text = ''\n  query: string | RegExp = ''\n  highlightTag: 'mark' | 'span' = 'mark'\n  highlightClass?: string\n  caseSensitive = false\n  wholeWord = false\n  maxHighlights = 0\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'highlight')\n  }\n\n  protected updated(changedProps: Map<string, unknown>) {\n    super.updated(changedProps)\n    if (\n      changedProps.has('text') ||\n      changedProps.has('query') ||\n      changedProps.has('caseSensitive') ||\n      changedProps.has('wholeWord') ||\n      changedProps.has('maxHighlights')\n    ) {\n      this.emitCounts()\n    }\n  }\n\n  private emitCounts() {\n    const pattern = buildPattern(this.query, this.caseSensitive, this.wholeWord)\n    if (!this.text || !pattern) {\n      this.dispatchEvent(new CustomEvent('match-count', { detail: { count: 0 }, bubbles: true, composed: true }))\n      this.dispatchEvent(new CustomEvent('total-match-count', { detail: { count: 0 }, bubbles: true, composed: true }))\n      return\n    }\n\n    let total = 0\n    let m: RegExpExecArray | null\n    while ((m = pattern.exec(this.text)) !== null) {\n      total++\n      if (m[0] === '') pattern.lastIndex++\n    }\n\n    const rendered = this.maxHighlights > 0 ? Math.min(total, this.maxHighlights) : total\n    this.dispatchEvent(new CustomEvent('match-count', { detail: { count: rendered }, bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('total-match-count', { detail: { count: total }, bubbles: true, composed: true }))\n  }\n\n  render() {\n    const text = this.text\n    const query = this.query\n\n    if (!text) return html``\n    if (!query) return html`<span part=\"base\">${text}</span>`\n\n    const pattern = buildPattern(query, this.caseSensitive, this.wholeWord)\n    if (!pattern) return html`<span part=\"base\">${text}</span>`\n\n    const segments: Segment[] = []\n    let last = 0\n    let count = 0\n    let m: RegExpExecArray | null\n\n    while ((m = pattern.exec(text)) !== null) {\n      if (m.index > last) {\n        segments.push({ text: text.slice(last, m.index), match: false })\n      }\n      segments.push({ text: m[0], match: true })\n      last = m.index + m[0].length\n      count++\n      if (this.maxHighlights > 0 && count >= this.maxHighlights) break\n      if (m[0] === '') pattern.lastIndex++\n    }\n\n    if (last < text.length) {\n      segments.push({ text: text.slice(last), match: false })\n    }\n\n    const matchClass = cn(\n      'bg-accent text-accent-foreground dark:bg-accent/30 dark:text-accent-foreground rounded px-0.5 font-medium',\n      this.highlightClass,\n    )\n\n    return html`\n      <span part=\"base\">\n        ${segments.map((seg) =>\n          seg.match\n            ? this.highlightTag === 'span'\n              ? html`<span part=\"match\" data-slot=\"highlight-match\" class=${matchClass}>${seg.text}</span>`\n              : html`<mark part=\"match\" data-slot=\"highlight-match\" class=${matchClass}>${seg.text}</mark>`\n            : html`${seg.text}`,\n        )}\n      </span>\n    `\n  }\n}\n\ncustomElements.get('uip-highlight') || customElements.define('uip-highlight', UipHighlight)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-highlight': UipHighlight\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/highlight/highlight.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
