{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "mentions",
  "title": "Mentions",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/mentions/mentions.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  UiPopoverAnchorComponent,\n  UiPopoverComponent,\n  UiPopoverContentComponent,\n  type PopoverDismissEvent,\n} from '@/ui/popover/popover.component'\nimport { uniqueId } from '@/ui/popper/popper'\nimport { getCaretRect, type CaretRect } from './caret-position'\n\nexport { UiMentionTagComponent } from './mention-tag.component'\n\nexport interface MentionOption {\n  value: string\n  label: string\n  description?: string\n  avatar?: string\n  email?: string\n  handle?: string\n  bio?: string\n  joined?: string\n  following?: number | string\n  followers?: number | string\n  verified?: boolean\n  disabled?: boolean\n  [key: string]: unknown\n}\n\n/**\n * Angular port of UIPKGE Mentions, 1:1 with the React component: a combobox textarea that\n * detects a trigger character (`@` by default, or any of `triggers`) at the caret, opens a\n * Popover anchored at the caret with the filtered options (static list, per-trigger map, or\n * debounced `loadOptions`), and on Enter / Tab / click replaces trigger + query with the\n * formatted token. ArrowUp / ArrowDown move the highlight (skipping disabled options) and\n * Escape closes. Focus stays in the textarea (the popover never steals it).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-mentions, [ui-mentions]',\n  standalone: true,\n  imports: [UiPopoverComponent, UiPopoverAnchorComponent, UiPopoverContentComponent],\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'mentions',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <textarea\n      #textarea\n      [value]=\"current()\"\n      [rows]=\"rows\"\n      [placeholder]=\"placeholder\"\n      [disabled]=\"disabled\"\n      [readOnly]=\"readOnly\"\n      role=\"combobox\"\n      aria-autocomplete=\"list\"\n      aria-haspopup=\"listbox\"\n      [attr.aria-expanded]=\"open()\"\n      [attr.aria-controls]=\"listboxId\"\n      [attr.aria-activedescendant]=\"open() && filtered.length > 0 ? optionId(highlightedIndex()) : null\"\n      class=\"border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-16 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n      (input)=\"onInput($event)\"\n      (keydown)=\"onKeyDown($event)\"\n      (scroll)=\"updateAnchor()\"\n    ></textarea>\n    <ui-popover [open]=\"open()\" (openChange)=\"open.set($event)\">\n      <div\n        ui-popover-anchor\n        aria-hidden=\"true\"\n        [style.display]=\"caretRect() ? null : 'none'\"\n        [style.position]=\"caretRect() ? 'fixed' : null\"\n        [style.top.px]=\"caretRect() ? caretRect()!.top + caretRect()!.height : null\"\n        [style.left.px]=\"caretRect()?.left ?? null\"\n        [style.width.px]=\"caretRect() ? 0 : null\"\n        [style.height.px]=\"caretRect() ? 0 : null\"\n        [style.pointer-events]=\"caretRect() ? 'none' : null\"\n      ></div>\n      <ui-popover-content\n        align=\"start\"\n        [sideOffset]=\"4\"\n        class=\"border-border/80 w-64 rounded-lg p-1 shadow-md\"\n        (openAutoFocus)=\"preventFocus($event)\"\n      >\n        <div [id]=\"listboxId\">\n          @if (totalLoading) {\n            <div class=\"text-muted-foreground px-2 py-3 text-sm\" role=\"status\">Loading...</div>\n          } @else if (filtered.length === 0) {\n            <div class=\"text-muted-foreground px-2 py-3 text-sm\" role=\"status\">No matches</div>\n          } @else {\n            <ul class=\"max-h-64 overflow-auto\" role=\"listbox\" aria-label=\"Mentions\">\n              @for (opt of filtered; track opt.value; let i = $index) {\n                <li\n                  [id]=\"optionId(i)\"\n                  role=\"option\"\n                  [attr.aria-selected]=\"i === highlightedIndex()\"\n                  [attr.aria-disabled]=\"opt.disabled || null\"\n                  [class]=\"optionClass(opt, i)\"\n                  (mouseenter)=\"!opt.disabled && highlightedIndex.set(i)\"\n                  (mousedown)=\"$event.preventDefault(); !opt.disabled && insert(opt)\"\n                >\n                  @if (opt.avatar) {\n                    <img [src]=\"opt.avatar\" alt=\"\" class=\"size-6 rounded-full object-cover\" />\n                  }\n                  <div class=\"min-w-0 flex-1\">\n                    <div class=\"truncate font-medium\">{{ opt.label }}</div>\n                    @if (opt.description || opt.email) {\n                      <div class=\"text-muted-foreground truncate text-xs\">{{ opt.description || opt.email }}</div>\n                    }\n                  </div>\n                </li>\n              }\n            </ul>\n          }\n        </div>\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiMentionsComponent<O extends MentionOption = MentionOption> implements OnChanges, OnDestroy {\n  /** Controlled textarea value (pair with `valueChange`). */\n  @Input() value = ''\n  @Input() options: O[] | Record<string, O[]> = []\n  @Input() triggers: string[] = ['@']\n  @Input() triggerPrefixes?: Record<string, string>\n  @Input() prefix = '@'\n  @Input() rows = 4\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input() loadOptions?: (query: string, trigger: string) => Promise<O[]>\n  @Input() format?: (option: O, trigger: string) => string\n  @Input() placeholder = ''\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) readOnly = false\n  @Input('class') className?: string\n\n  /** Fires with the next textarea value on every input + on insert (React `onValueChange`). */\n  @Output() valueChange = new EventEmitter<string>()\n  /**\n   * Fires when an option is committed into the text (React `onSelect`). Not named `select`:\n   * the textarea's native select event bubbles to this host and would reach `(select)` too.\n   */\n  @Output() mentionSelect = new EventEmitter<O>()\n  /** Fires whenever the active mention query changes (React `onSearch`). */\n  @Output() search = new EventEmitter<{ trigger: string; query: string }>()\n\n  @ViewChild('textarea', { static: true }) private textareaRef!: ElementRef<HTMLTextAreaElement>\n\n  readonly open = signal(false)\n  readonly activeTrigger = signal('')\n  readonly query = signal('')\n  readonly highlightedIndex = signal(0)\n  readonly asyncResults = signal<O[]>([])\n  readonly isAsyncLoading = signal(false)\n  readonly caretRect = signal<CaretRect | null>(null)\n  /** Latest text: the `value` input, advanced by typing / inserts until the parent echoes it. */\n  readonly current = signal('')\n  private triggerIndex = -1\n  private asyncToken = 0\n  private debounceTimer: ReturnType<typeof setTimeout> | null = null\n  readonly listboxId = uniqueId('mentions-listbox')\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['value']) this.current.set(this.value ?? '')\n  }\n\n  ngOnDestroy(): void {\n    if (this.debounceTimer) clearTimeout(this.debounceTimer)\n  }\n\n  get hostClass(): string {\n    return cn('block', 'relative w-full', this.className)\n  }\n\n  optionId(i: number): string {\n    return `${this.listboxId}-opt-${i}`\n  }\n\n  private get currentOptionsList(): O[] {\n    const options = this.options\n    if (!options) return []\n    if (Array.isArray(options)) return options\n    return options[this.activeTrigger()] ?? []\n  }\n\n  get filtered(): O[] {\n    if (this.loadOptions) return this.asyncResults()\n    const source = this.currentOptionsList\n    const query = this.query()\n    if (!query) return source\n    const q = query.toLowerCase()\n    return source.filter(\n      (o) =>\n        o.label.toLowerCase().includes(q) ||\n        o.value.toLowerCase().includes(q) ||\n        (o.email && o.email.toLowerCase().includes(q)),\n    )\n  }\n\n  get totalLoading(): boolean {\n    return this.loading || this.isAsyncLoading()\n  }\n\n  optionClass(opt: O, i: number): string {\n    return cn(\n      'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',\n      i === this.highlightedIndex() && !opt.disabled ? 'bg-accent text-accent-foreground' : '',\n      opt.disabled ? 'cursor-not-allowed opacity-50' : '',\n    )\n  }\n\n  /** React: `onOpenAutoFocus={(e) => e.preventDefault()}` keeps the caret in the textarea. */\n  preventFocus(e: PopoverDismissEvent): void {\n    e.preventDefault()\n  }\n\n  private moveHighlight(delta: number): void {\n    const list = this.filtered\n    const len = list.length\n    if (len === 0) return\n    let i = this.highlightedIndex()\n    for (let n = 0; n < len; n++) {\n      i = (i + delta + len) % len\n      if (!list[i]?.disabled) {\n        document.getElementById(this.optionId(i))?.scrollIntoView?.({ block: 'nearest' })\n        this.highlightedIndex.set(i)\n        return\n      }\n    }\n  }\n\n  private findActiveMention(val: string, caret: number): { trigger: string; index: number; query: string } | null {\n    for (let i = caret - 1; i >= 0; i--) {\n      const ch = val[i]!\n      if (this.triggers.includes(ch)) {\n        const before = i === 0 ? '' : val[i - 1]!\n        if (i === 0 || /\\s/.test(before)) return { trigger: ch, index: i, query: val.substring(i + 1, caret) }\n        return null\n      }\n      if (/\\s/.test(ch)) return null\n    }\n    return null\n  }\n\n  updateAnchor(): void {\n    const ta = this.textareaRef.nativeElement\n    this.caretRect.set(getCaretRect(ta, ta.selectionStart ?? 0))\n  }\n\n  private async runAsync(trigger: string, q: string): Promise<void> {\n    if (!this.loadOptions) return\n    const token = ++this.asyncToken\n    this.isAsyncLoading.set(true)\n    try {\n      const results = await this.loadOptions(q, trigger)\n      if (token === this.asyncToken) this.asyncResults.set(results)\n    } finally {\n      if (token === this.asyncToken) this.isAsyncLoading.set(false)\n    }\n  }\n\n  private scheduleAsync(trigger: string, q: string): void {\n    if (this.debounceTimer) clearTimeout(this.debounceTimer)\n    this.debounceTimer = setTimeout(() => void this.runAsync(trigger, q), 200)\n  }\n\n  onInput(e: Event): void {\n    const ta = e.target as HTMLTextAreaElement\n    const next = ta.value\n    this.current.set(next)\n    this.valueChange.emit(next)\n    const match = this.findActiveMention(next, ta.selectionStart ?? 0)\n    if (match) {\n      // Anchor at the caret before opening so the first placement is already right.\n      this.updateAnchor()\n      this.activeTrigger.set(match.trigger)\n      this.triggerIndex = match.index\n      this.query.set(match.query)\n      this.open.set(true)\n      this.search.emit({ trigger: match.trigger, query: match.query })\n      if (this.loadOptions) this.scheduleAsync(match.trigger, match.query)\n      requestAnimationFrame(() => this.updateAnchor())\n    } else {\n      this.open.set(false)\n    }\n  }\n\n  private defaultFormat(option: O, trigger: string): string {\n    const resolvedPrefix = this.triggerPrefixes?.[trigger] ?? trigger ?? this.prefix ?? '@'\n    return `${resolvedPrefix}${option.value} `\n  }\n\n  insert(option: O): void {\n    const ta = this.textareaRef.nativeElement\n    const caret = ta.selectionStart ?? 0\n    const value = this.current()\n    const before = value.substring(0, this.triggerIndex)\n    const after = value.substring(caret)\n    const trigger = this.activeTrigger()\n    const token = this.format ? this.format(option, trigger) : this.defaultFormat(option, trigger)\n    const next = before + token + after\n    this.current.set(next)\n    this.valueChange.emit(next)\n    this.mentionSelect.emit(option)\n    this.open.set(false)\n    requestAnimationFrame(() => {\n      const pos = before.length + token.length\n      ta.focus()\n      ta.setSelectionRange(pos, pos)\n    })\n  }\n\n  onKeyDown(e: KeyboardEvent): void {\n    if (!this.open()) return\n    if (e.key === 'Escape') {\n      e.preventDefault()\n      this.open.set(false)\n      return\n    }\n    const list = this.filtered\n    if (list.length === 0) return\n    if (e.key === 'ArrowDown') {\n      e.preventDefault()\n      this.moveHighlight(1)\n    } else if (e.key === 'ArrowUp') {\n      e.preventDefault()\n      this.moveHighlight(-1)\n    } else if (e.key === 'Enter' || e.key === 'Tab') {\n      e.preventDefault()\n      const opt = list[this.highlightedIndex()]\n      if (opt && !opt.disabled) this.insert(opt)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/mentions/mentions.component.ts"
    },
    {
      "path": "packages/registry-angular/components/mentions/mention-tag.component.ts",
      "content": "import {\n  AfterViewChecked,\n  Component,\n  ElementRef,\n  Input,\n  TemplateRef,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  UiHoverCardComponent,\n  UiHoverCardContentComponent,\n  UiHoverCardTriggerComponent,\n} from '@/ui/hover-card/hover-card.component'\nimport { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\n\n/**\n * Angular port of UIPKGE MentionTag, 1:1 with the React component: an inline chip (an\n * `<a>` when `href` is set, else a `<span>`) showing `trigger` + name, which reveals a\n * Twitter/X-style profile HoverCard (avatar, Follow toggle, name + verified badge, handle,\n * email, bio, joined date, following / followers) after `openDelay`. `popover=false`\n * renders the bare chip; `popupContent` replaces the card body. Projected content replaces\n * the default chip text (React `children`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-mention-tag, [ui-mention-tag]',\n  standalone: true,\n  imports: [\n    UiHoverCardComponent,\n    UiHoverCardTriggerComponent,\n    UiHoverCardContentComponent,\n    UiAvatarComponent,\n    UiAvatarImageComponent,\n    UiAvatarFallbackComponent,\n    UiRenderTemplateDirective,\n  ],\n  // React renders the chip itself (no wrapper element).\n  host: { '[class]': '\"contents\"' },\n  template: `\n    <ng-template #chipContent>\n      <ng-content>\n        <span class=\"text-primary font-semibold\">{{ trigger }}</span>\n        <span>{{ name || handle || email }}</span>\n      </ng-content>\n    </ng-template>\n\n    @if (popover) {\n      <ui-hover-card [openDelay]=\"openDelay\" [closeDelay]=\"closeDelay\">\n        @if (href) {\n          <a ui-hover-card-trigger [attr.href]=\"href\" data-uipkge=\"\" data-slot=\"mention-tag\" [class]=\"chipClass\"\n            ><ng-container [uiRenderTemplate]=\"chipContent\"\n          /></a>\n        } @else {\n          <span ui-hover-card-trigger data-uipkge=\"\" data-slot=\"mention-tag\" [class]=\"chipClass\"\n            ><ng-container [uiRenderTemplate]=\"chipContent\"\n          /></span>\n        }\n        <ui-hover-card-content align=\"start\" [sideOffset]=\"6\" class=\"border-border/80 w-80 rounded-xl p-4 shadow-lg\">\n          @if (popupContent) {\n            <ng-container [uiRenderTemplate]=\"popupContent\" />\n          } @else {\n            <div class=\"space-y-3\">\n              <div class=\"flex items-start justify-between gap-3\">\n                <ui-avatar class=\"ring-border/50 size-12 ring-2\">\n                  @if (avatar) {\n                    <ui-avatar-image [src]=\"avatar\" [alt]=\"name || handle || ''\" />\n                  }\n                  <ui-avatar-fallback class=\"text-sm font-semibold\">{{ initials }}</ui-avatar-fallback>\n                </ui-avatar>\n                <button\n                  type=\"button\"\n                  [class]=\"followClass\"\n                  (click)=\"$event.stopPropagation(); isFollowing.set(!isFollowing())\"\n                >\n                  {{ isFollowing() ? 'Following' : 'Follow' }}\n                </button>\n              </div>\n\n              <div>\n                <div class=\"flex items-center gap-1\">\n                  <span class=\"text-foreground text-sm font-bold tracking-tight\">{{ name || handle }}</span>\n                  @if (verified) {\n                    <span class=\"text-primary inline-flex\" title=\"Verified\">\n                      <svg class=\"size-3.5 fill-current\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                        <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" />\n                      </svg>\n                    </span>\n                  }\n                </div>\n                <p class=\"text-muted-foreground font-mono text-xs\">{{ formattedHandle }}</p>\n                @if (email) {\n                  <p class=\"text-muted-foreground mt-0.5 text-xs\">{{ email }}</p>\n                }\n              </div>\n\n              @if (bio) {\n                <p class=\"text-foreground/90 text-xs leading-relaxed\">{{ bio }}</p>\n              }\n\n              @if (joined || location) {\n                <div class=\"text-muted-foreground flex items-center gap-4 text-xs\">\n                  @if (joined) {\n                    <div class=\"flex items-center gap-1\">\n                      <svg\n                        xmlns=\"http://www.w3.org/2000/svg\"\n                        width=\"24\"\n                        height=\"24\"\n                        viewBox=\"0 0 24 24\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        stroke-width=\"2\"\n                        stroke-linecap=\"round\"\n                        stroke-linejoin=\"round\"\n                        class=\"lucide lucide-calendar size-3.5 opacity-70\"\n                        aria-hidden=\"true\"\n                      >\n                        <path d=\"M8 2v4\" />\n                        <path d=\"M16 2v4\" />\n                        <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" />\n                        <path d=\"M3 10h18\" />\n                      </svg>\n                      <span>Joined {{ joined }}</span>\n                    </div>\n                  }\n                </div>\n              }\n\n              @if (following !== undefined || followers !== undefined) {\n                <div class=\"border-border/50 flex items-center gap-4 border-t pt-1 text-xs\">\n                  @if (following !== undefined) {\n                    <div>\n                      <span class=\"text-foreground font-bold\">{{ following }}</span>\n                      <span class=\"text-muted-foreground ml-1\">Following</span>\n                    </div>\n                  }\n                  @if (followers !== undefined) {\n                    <div>\n                      <span class=\"text-foreground font-bold\">{{ followers }}</span>\n                      <span class=\"text-muted-foreground ml-1\">Followers</span>\n                    </div>\n                  }\n                </div>\n              }\n            </div>\n          }\n        </ui-hover-card-content>\n      </ui-hover-card>\n    } @else if (href) {\n      <a [attr.href]=\"href\" data-uipkge=\"\" data-slot=\"mention-tag\" [class]=\"chipClass\"\n        ><ng-container [uiRenderTemplate]=\"chipContent\"\n      /></a>\n    } @else {\n      <span data-uipkge=\"\" data-slot=\"mention-tag\" [class]=\"chipClass\"\n        ><ng-container [uiRenderTemplate]=\"chipContent\"\n      /></span>\n    }\n  `,\n})\nexport class UiMentionTagComponent implements AfterViewChecked {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  @Input() trigger = '@'\n  @Input() name?: string\n  @Input() handle?: string\n  @Input() email?: string\n  @Input() avatar?: string\n  @Input() bio?: string\n  @Input() joined?: string\n  @Input() location?: string\n  @Input() following?: number | string\n  @Input() followers?: number | string\n  @Input({ transform: booleanAttribute }) verified = false\n  @Input() href?: string\n  @Input({ transform: booleanAttribute }) popover = true\n  @Input() openDelay = 150\n  @Input() closeDelay = 100\n  /** Replaces the default profile card body (React `popupContent` node). */\n  @Input() popupContent?: TemplateRef<unknown> | null\n  @Input('class') className?: string\n\n  readonly isFollowing = signal(false)\n\n  ngAfterViewChecked(): void {\n    // Radix Slot: the chip's data-slot wins over HoverCardTrigger's (Angular host bindings would win).\n    const chip = this.el.querySelector(':scope > ui-hover-card > [data-slot=\"hover-card-trigger\"]')\n    chip?.setAttribute('data-slot', 'mention-tag')\n  }\n\n  get formattedHandle(): string {\n    if (!this.handle && !this.name) return ''\n    const h = this.handle || this.name || ''\n    return h.startsWith(this.trigger) ? h : `${this.trigger}${h}`\n  }\n\n  get initials(): string {\n    return (this.name || this.handle || 'U').slice(0, 2).toUpperCase()\n  }\n\n  get chipClass(): string {\n    return cn(\n      'inline-flex cursor-pointer items-center gap-0.5 rounded px-1.5 py-0.5 text-sm font-medium transition-colors select-none',\n      'bg-muted/70 text-foreground hover:bg-accent hover:text-accent-foreground',\n      'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',\n      this.className,\n    )\n  }\n\n  get followClass(): string {\n    return cn(\n      'h-8 rounded-full px-3.5 text-xs font-semibold transition-[transform,background-color] duration-150 active:scale-95',\n      this.isFollowing()\n        ? 'border-border text-foreground hover:bg-muted border bg-transparent'\n        : 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',\n    )\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/mentions/mention-tag.component.ts"
    },
    {
      "path": "packages/registry-angular/components/mentions/caret-position.ts",
      "content": "const PROPERTIES_TO_COPY = [\n  'direction',\n  'boxSizing',\n  'width',\n  'height',\n  'overflowX',\n  'overflowY',\n  'borderTopWidth',\n  'borderRightWidth',\n  'borderBottomWidth',\n  'borderLeftWidth',\n  'borderStyle',\n  'paddingTop',\n  'paddingRight',\n  'paddingBottom',\n  'paddingLeft',\n  'fontStyle',\n  'fontVariant',\n  'fontWeight',\n  'fontStretch',\n  'fontSize',\n  'fontSizeAdjust',\n  'lineHeight',\n  'fontFamily',\n  'textAlign',\n  'textTransform',\n  'textIndent',\n  'textDecoration',\n  'letterSpacing',\n  'wordSpacing',\n  'tabSize',\n  'whiteSpace',\n  'wordBreak',\n  'wordWrap',\n] as const\n\nexport interface CaretRect {\n  top: number\n  left: number\n  height: number\n}\n\nexport function getCaretRect(textarea: HTMLTextAreaElement, position: number): CaretRect {\n  const div = document.createElement('div')\n  document.body.appendChild(div)\n\n  const style = div.style\n  const computed = window.getComputedStyle(textarea)\n\n  style.position = 'absolute'\n  style.visibility = 'hidden'\n  style.whiteSpace = 'pre-wrap'\n  style.wordWrap = 'break-word'\n  style.top = '0'\n  style.left = '0'\n\n  for (const prop of PROPERTIES_TO_COPY) {\n    ;(style as any)[prop] = (computed as any)[prop]\n  }\n\n  style.overflow = 'hidden'\n\n  const text = textarea.value.substring(0, position)\n  div.textContent = text\n\n  const span = document.createElement('span')\n  span.textContent = textarea.value.substring(position) || '.'\n  div.appendChild(span)\n\n  const spanRect = span.getBoundingClientRect()\n  const divRect = div.getBoundingClientRect()\n  const taRect = textarea.getBoundingClientRect()\n\n  const result: CaretRect = {\n    top: taRect.top + (spanRect.top - divRect.top) - textarea.scrollTop,\n    left: taRect.left + (spanRect.left - divRect.left) - textarea.scrollLeft,\n    height: spanRect.height,\n  }\n\n  document.body.removeChild(div)\n  return result\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/mentions/caret-position.ts"
    },
    {
      "path": "packages/registry-angular/components/mentions/index.ts",
      "content": "export { UiMentionsComponent, type MentionOption } from './mentions.component'\nexport { UiMentionTagComponent } from './mention-tag.component'\nexport * from './caret-position'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/mentions/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popover.json",
    "https://uipkge.dev/r/angular/hover-card.json",
    "https://uipkge.dev/r/angular/avatar.json",
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Textarea with trigger-character autocomplete and companion MentionTag with Twitter/X-style hover profile card popup.",
  "categories": [
    "form"
  ]
}