{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "highlight",
  "title": "Highlight",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/highlight/highlight.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  OnChanges,\n  Output,\n  booleanAttribute,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\nexport interface HighlightSegment {\n  text: string\n  match: boolean\n}\n\nexport function 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 = query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n  const body = wholeWord ? `\\\\b${escaped}\\\\b` : escaped\n  return new RegExp(body, caseSensitive ? 'g' : 'gi')\n}\n\n/**\n * Highlights matching substrings of `text` (React `Highlight`): string or RegExp queries,\n * case-sensitive and whole-word matching, `mark` / `span` wrapper, a max-highlight cap.\n * `matchCount` fires with the rendered count (after the cap), `totalMatchCount` with the total.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-highlight, [ui-highlight]',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'highlight',\n    '[class]': 'hostClass',\n  },\n  template: `@for (seg of segments; track $index) {\n    @if (!seg.match) {\n      <ng-container>{{ seg.text }}</ng-container>\n    } @else if (highlightTag === 'span') {\n      <span data-slot=\"highlight-match\" [class]=\"matchClass\" [style]=\"highlightStyle ?? null\">{{ seg.text }}</span>\n    } @else {\n      <mark data-slot=\"highlight-match\" [class]=\"matchClass\" [style]=\"highlightStyle ?? null\">{{ seg.text }}</mark>\n    }\n  }`,\n})\nexport class UiHighlightComponent implements OnChanges {\n  /** Text to search within. */\n  @Input({ required: true }) text = ''\n  /** Query string or RegExp to highlight. */\n  @Input({ required: true }) query: string | RegExp = ''\n  /** HTML tag used to wrap matched substrings. */\n  @Input() highlightTag: 'mark' | 'span' = 'mark'\n  /** Class applied to each highlight wrapper. */\n  @Input() highlightClass?: string\n  /** Inline style applied to each highlight wrapper. */\n  @Input() highlightStyle?: string | Record<string, string>\n  @Input({ transform: booleanAttribute }) caseSensitive = false\n  @Input({ transform: booleanAttribute }) wholeWord = false\n  /** Cap the number of highlights rendered. 0 = unlimited. */\n  @Input() maxHighlights = 0\n  @Input('class') className?: string\n\n  /** Fired with the number of highlights actually rendered (after maxHighlights cap). React `onMatchCount`. */\n  @Output() matchCount = new EventEmitter<number>()\n  /** Fired with the total match count (before maxHighlights cap). React `onTotalMatchCount`. */\n  @Output() totalMatchCount = new EventEmitter<number>()\n\n  segments: HighlightSegment[] = []\n  total = 0\n  private emitted: { rendered?: number; total?: number } = {}\n\n  get hostClass(): string {\n    return cn(this.className)\n  }\n\n  get matchClass(): string {\n    return 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\n  get renderedCount(): number {\n    return this.segments.filter((s) => s.match).length\n  }\n\n  ngOnChanges(): void {\n    this.segments = this.computeSegments()\n    this.total = this.computeTotal()\n    // Like React's effects: report after render, and only when a count actually changed.\n    const rendered = this.renderedCount\n    const total = this.total\n    queueMicrotask(() => {\n      if (this.emitted.rendered !== rendered) this.matchCount.emit((this.emitted.rendered = rendered))\n      if (this.emitted.total !== total) this.totalMatchCount.emit((this.emitted.total = total))\n    })\n  }\n\n  private computeSegments(): HighlightSegment[] {\n    const text = this.text\n    if (!text) return []\n    if (!this.query) return [{ text, match: false }]\n    const pattern = buildPattern(this.query, this.caseSensitive, this.wholeWord)\n    if (!pattern) return [{ text, match: false }]\n    const out: HighlightSegment[] = []\n    let last = 0\n    let count = 0\n    let m: RegExpExecArray | null\n    while ((m = pattern.exec(text)) !== null) {\n      if (m.index > last) out.push({ text: text.slice(last, m.index), match: false })\n      out.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    if (last < text.length) out.push({ text: text.slice(last), match: false })\n    return out\n  }\n\n  private computeTotal(): number {\n    if (!this.text || !this.query) return 0\n    const pattern = buildPattern(this.query, this.caseSensitive, this.wholeWord)\n    if (!pattern) return 0\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    return total\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/highlight/highlight.component.ts"
    },
    {
      "path": "packages/registry-angular/components/highlight/index.ts",
      "content": "export { UiHighlightComponent } from './highlight.component'\nexport type { HighlightSegment } from './highlight.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/highlight/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}