{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "mentions",
  "title": "Mentions",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/mentions/Mentions.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLTextareaAttributes } from 'svelte/elements'\n  import type { MentionOption } from './index'\n\n  // `onselect` is omitted: the native select-event handler collides with the\n  // (option) select callback.\n  export interface MentionsProps<O extends MentionOption = MentionOption>\n    extends Omit<HTMLTextareaAttributes, 'value' | 'prefix' | 'onselect'> {\n    value?: string\n    onValueChange?: (value: string) => void\n    options?: O[] | Record<string, O[]>\n    triggers?: string[]\n    triggerPrefixes?: Record<string, string>\n    prefix?: string\n    loading?: boolean\n    loadOptions?: (query: string, trigger: string) => Promise<O[]>\n    format?: (option: O, trigger: string) => string\n    onselect?: (option: O) => void\n    onsearch?: (payload: { trigger: string; query: string }) => void\n    header?: Snippet<[{ trigger: string; query: string }]>\n    loadingSnippet?: Snippet\n    empty?: Snippet<[{ trigger: string; query: string }]>\n    option?: Snippet<[{ option: O; index: number; active: boolean; trigger: string }]>\n    footer?: Snippet<[{ trigger: string; count: number }]>\n    ref?: HTMLTextAreaElement | null\n  }\n\n  let mentionIdCounter = 0\n</script>\n\n<script lang=\"ts\" generics=\"O extends MentionOption\">\n  import { tick } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { getCaretRect } from './caret-position'\n\n  let {\n    class: className,\n    value = $bindable(''),\n    onValueChange,\n    options = [],\n    triggers = ['@'],\n    triggerPrefixes,\n    prefix = '@',\n    rows = 4,\n    loading = false,\n    loadOptions,\n    format,\n    placeholder = '',\n    disabled = false,\n    readonly = false,\n    onselect,\n    onsearch,\n    header,\n    loadingSnippet,\n    empty,\n    option,\n    footer,\n    oninput,\n    onkeydown,\n    onscroll,\n    ref = $bindable(null),\n    ...restProps\n  }: MentionsProps<O> = $props()\n\n  const listboxId = `mentions-listbox-${++mentionIdCounter}`\n  function optionId(i: number) {\n    return `${listboxId}-opt-${i}`\n  }\n\n  let textarea: HTMLTextAreaElement | null = $state(null)\n  let open = $state(false)\n  let activeTrigger = $state('')\n  let query = $state('')\n  let triggerIndex = $state(-1)\n  let highlightedIndex = $state(0)\n  let asyncResults = $state<O[]>([])\n  let isAsyncLoading = $state(false)\n  // Caret-anchored dropdown offset relative to the wrapper.\n  let anchorTop = $state(0)\n  let anchorLeft = $state(0)\n\n  $effect(() => {\n    ref = textarea\n  })\n\n  const currentOptionsList = $derived.by((): O[] => {\n    if (!options) return []\n    if (Array.isArray(options)) return options as O[]\n    if (typeof options === 'object') {\n      const list = (options as Record<string, O[]>)[activeTrigger]\n      return list ?? []\n    }\n    return []\n  })\n\n  const filtered = $derived.by((): O[] => {\n    if (loadOptions) return asyncResults as O[]\n    const source = currentOptionsList\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  const totalLoading = $derived(loading || isAsyncLoading)\n\n  function findActiveMention(text: string, caret: number): { trigger: string; index: number; query: string } | null {\n    for (let i = caret - 1; i >= 0; i--) {\n      const ch = text[i]!\n      if (triggers.includes(ch)) {\n        const before = i === 0 ? '' : text[i - 1]!\n        if (i === 0 || /\\s/.test(before)) {\n          return { trigger: ch, index: i, query: text.substring(i + 1, caret) }\n        }\n        return null\n      }\n      if (/\\s/.test(ch)) return null\n    }\n    return null\n  }\n\n  function updateAnchor() {\n    if (!textarea) return\n    const rect = getCaretRect(textarea, textarea.selectionStart ?? 0)\n    const wrap = textarea.getBoundingClientRect()\n    // Position the panel just under the caret line, clamped inside the wrapper.\n    anchorTop = Math.min(rect.top - wrap.top + rect.height + 4, Math.max(0, wrap.height - 8))\n    anchorLeft = Math.max(0, Math.min(rect.left - wrap.left, Math.max(0, wrap.width - 256)))\n  }\n\n  let asyncToken = 0\n  async function runAsync(trigger: string, q: string) {\n    if (!loadOptions) return\n    const token = ++asyncToken\n    isAsyncLoading = true\n    try {\n      const results = await loadOptions(q, trigger)\n      if (token === asyncToken) asyncResults = results\n    } finally {\n      if (token === asyncToken) isAsyncLoading = false\n    }\n  }\n\n  let debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n  function scheduleAsync(trigger: string, q: string) {\n    if (debounceTimer) clearTimeout(debounceTimer)\n    debounceTimer = setTimeout(() => runAsync(trigger, q), 200)\n  }\n\n  function firstEnabledIndex() {\n    const i = filtered.findIndex((o) => !o.disabled)\n    return i === -1 ? 0 : i\n  }\n\n  function handleInput(e: Event) {\n    const ta = e.target as HTMLTextAreaElement\n    value = ta.value\n    onValueChange?.(value)\n\n    const match = findActiveMention(ta.value, ta.selectionStart ?? 0)\n    if (match) {\n      open = true\n      activeTrigger = match.trigger\n      triggerIndex = match.index\n      query = match.query\n      highlightedIndex = firstEnabledIndex()\n      onsearch?.({ trigger: match.trigger, query: match.query })\n      if (loadOptions) scheduleAsync(match.trigger, match.query)\n      tick().then(updateAnchor)\n    } else {\n      open = false\n    }\n  }\n\n  function moveHighlight(delta: number) {\n    const len = filtered.length\n    if (len === 0) return\n    let i = highlightedIndex\n    for (let n = 0; n < len; n++) {\n      i = (i + delta + len) % len\n      if (!filtered[i]?.disabled) {\n        highlightedIndex = i\n        tick().then(() => {\n          document.getElementById(optionId(i))?.scrollIntoView({ block: 'nearest' })\n        })\n        return\n      }\n    }\n  }\n\n  function handleKeydown(e: KeyboardEvent) {\n    if (open) {\n      // Escape must work even when there are no matches / still loading.\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        open = false\n      } else if (filtered.length > 0) {\n        if (e.key === 'ArrowDown') {\n          e.preventDefault()\n          moveHighlight(1)\n        } else if (e.key === 'ArrowUp') {\n          e.preventDefault()\n          moveHighlight(-1)\n        } else if (e.key === 'Enter' || e.key === 'Tab') {\n          e.preventDefault()\n          const opt = filtered[highlightedIndex]\n          if (opt && !opt.disabled) insert(opt)\n        }\n      }\n    }\n  }\n\n  function defaultFormat(opt: O, trigger: string) {\n    const resolvedPrefix = triggerPrefixes?.[trigger] ?? trigger ?? prefix ?? '@'\n    return `${resolvedPrefix}${opt.value} `\n  }\n\n  function insert(opt: O) {\n    const ta = textarea\n    if (!ta) return\n    const caret = ta.selectionStart ?? 0\n    const before = value.substring(0, triggerIndex)\n    const after = value.substring(caret)\n    const token = (format ?? defaultFormat)(opt, activeTrigger)\n    value = before + token + after\n    onValueChange?.(value)\n    onselect?.(opt)\n    open = false\n    tick().then(() => {\n      const pos = before.length + token.length\n      ta.focus()\n      ta.setSelectionRange(pos, pos)\n    })\n  }\n\n  // Close on outside pointer down (the hand-rolled dropdown has no portal).\n  $effect(() => {\n    if (!open) return\n    function onPointerDown(e: PointerEvent) {\n      if (textarea && !textarea.parentElement?.contains(e.target as Node)) open = false\n    }\n    document.addEventListener('pointerdown', onPointerDown)\n    return () => document.removeEventListener('pointerdown', onPointerDown)\n  })\n\n  $effect(() => {\n    return () => {\n      if (debounceTimer) clearTimeout(debounceTimer)\n    }\n  })\n</script>\n\n<div class={cn('relative w-full', className)} data-uipkge=\"\" data-slot=\"mentions\">\n  <textarea\n    bind:this={textarea}\n    {value}\n    {rows}\n    {placeholder}\n    {disabled}\n    {readonly}\n    role=\"combobox\"\n    aria-autocomplete=\"list\"\n    aria-haspopup=\"listbox\"\n    aria-expanded={open}\n    aria-controls={listboxId}\n    aria-activedescendant={open && filtered.length > 0 ? optionId(highlightedIndex) : undefined}\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    oninput={(e) => {\n      handleInput(e)\n      oninput?.(e)\n    }}\n    onkeydown={(e) => {\n      handleKeydown(e)\n      onkeydown?.(e)\n    }}\n    onscroll={(e) => {\n      updateAnchor()\n      onscroll?.(e)\n    }}\n    {...restProps}\n  ></textarea>\n  {#if open}\n    <!-- Hand-rolled replacement for Popover + PopoverAnchor + PopoverContent:\n         no headless float library is installed in the Svelte registry yet, so\n         the listbox is absolutely positioned at the caret inside the wrapper. -->\n    <div\n      data-slot=\"mentions-content\"\n      role=\"presentation\"\n      class=\"bg-popover text-popover-foreground border-border/80 absolute z-50 w-64 rounded-lg border p-1 shadow-md\"\n      style:top=\"{anchorTop}px\"\n      style:left=\"{anchorLeft}px\"\n    >\n      <div id={listboxId}>\n        {#if header}\n          {@render header({ trigger: activeTrigger, query })}\n        {/if}\n\n        {#if totalLoading}\n          <div class=\"text-muted-foreground px-2 py-3 text-sm\" role=\"status\">\n            {#if loadingSnippet}\n              {@render loadingSnippet()}\n            {:else}\n              Loading...\n            {/if}\n          </div>\n        {:else if filtered.length === 0}\n          <div class=\"text-muted-foreground px-2 py-3 text-sm\" role=\"status\">\n            {#if empty}\n              {@render empty({ trigger: activeTrigger, query })}\n            {:else}\n              No matches\n            {/if}\n          </div>\n        {:else}\n          <ul class=\"max-h-64 overflow-auto\" role=\"listbox\" aria-label=\"Mentions\">\n            {#each filtered as opt, i (opt.value)}\n              <li\n                id={optionId(i)}\n                role=\"option\"\n                aria-selected={i === highlightedIndex}\n                aria-disabled={opt.disabled || undefined}\n                class={[\n                  'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',\n                  i === highlightedIndex && !opt.disabled ? 'bg-accent text-accent-foreground' : '',\n                  opt.disabled ? 'cursor-not-allowed opacity-50' : '',\n                ]\n                  .filter(Boolean)\n                  .join(' ')}\n                onmouseenter={() => {\n                  if (!opt.disabled) highlightedIndex = i\n                }}\n                onmousedown={(e) => {\n                  e.preventDefault()\n                  if (!opt.disabled) insert(opt)\n                }}\n              >\n                {#if option}\n                  {@render option({ option: opt, index: i, active: i === highlightedIndex, trigger: activeTrigger })}\n                {:else}\n                  {#if opt.avatar}\n                    <img src={opt.avatar} alt=\"\" class=\"size-6 rounded-full object-cover\" />\n                  {/if}\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\">\n                        {opt.description || opt.email}\n                      </div>\n                    {/if}\n                  </div>\n                {/if}\n              </li>\n            {/each}\n          </ul>\n        {/if}\n\n        {#if footer}\n          {@render footer({ trigger: activeTrigger, count: filtered.length })}\n        {/if}\n      </div>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/mentions/Mentions.svelte"
    },
    {
      "path": "packages/registry-svelte/components/mentions/MentionTag.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes, HTMLAnchorAttributes } from 'svelte/elements'\n\n  // `popover` is omitted: the native popover attribute collides with the\n  // boolean profile-card prop.\n  export interface MentionTagProps extends Omit<HTMLAttributes<HTMLElement>, 'popover'> {\n    trigger?: string\n    name?: string\n    handle?: string\n    email?: string\n    avatar?: string\n    bio?: string\n    joined?: string\n    location?: string\n    following?: number | string\n    followers?: number | string\n    verified?: boolean\n    href?: string\n    popover?: boolean\n    openDelay?: number\n    closeDelay?: number\n    /** Popup card content override (React parity name). */\n    popupContent?: Snippet\n    /** Alias of `popupContent` (legacy Svelte name). */\n    popup?: Snippet\n    children?: Snippet\n    ref?: HTMLElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Calendar } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    trigger = '@',\n    name,\n    handle,\n    email,\n    avatar,\n    bio,\n    joined,\n    location: _location,\n    following,\n    followers,\n    verified,\n    href,\n    popover = true,\n    openDelay = 150,\n    closeDelay = 100,\n    popupContent,\n    popup,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: MentionTagProps = $props()\n\n  const resolvedPopup = $derived(popupContent ?? popup)\n\n  // `_location` is accepted for API parity with the Vue twin; the current card\n  // layout only renders `joined` (same as Vue).\n\n  let isFollowing = $state(false)\n  let cardOpen = $state(false)\n  let openTimer: ReturnType<typeof setTimeout> | null = null\n  let closeTimer: ReturnType<typeof setTimeout> | null = null\n\n  function clearTimers() {\n    if (openTimer) clearTimeout(openTimer)\n    if (closeTimer) clearTimeout(closeTimer)\n    openTimer = null\n    closeTimer = null\n  }\n\n  function scheduleOpen() {\n    if (!popover) return\n    if (closeTimer) clearTimeout(closeTimer)\n    closeTimer = null\n    if (cardOpen || openTimer) return\n    openTimer = setTimeout(() => {\n      openTimer = null\n      cardOpen = true\n    }, openDelay)\n  }\n\n  function scheduleClose() {\n    if (openTimer) clearTimeout(openTimer)\n    openTimer = null\n    if (!cardOpen || closeTimer) return\n    closeTimer = setTimeout(() => {\n      closeTimer = null\n      cardOpen = false\n    }, closeDelay)\n  }\n\n  $effect(() => {\n    return () => clearTimers()\n  })\n\n  const formattedHandle = $derived.by(() => {\n    if (!handle && !name) return ''\n    const h = handle || name || ''\n    return h.startsWith(trigger) ? h : `${trigger}${h}`\n  })\n\n  const initials = $derived.by(() => {\n    const source = name || handle || 'U'\n    return source.slice(0, 2).toUpperCase()\n  })\n\n  const tagClass = $derived(\n    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      className,\n    ),\n  )\n\n  const plainTagClass = $derived(\n    cn(\n      'inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-sm font-medium transition-colors',\n      'bg-muted/70 text-foreground',\n      className,\n    ),\n  )\n</script>\n\n{#snippet tagContent()}\n  {#if children}\n    {@render children()}\n  {:else}\n    <span class=\"text-primary font-semibold\">{trigger}</span>\n    <span>{name || handle || email}</span>\n  {/if}\n{/snippet}\n\n{#snippet profileCard()}\n  {#if resolvedPopup}\n    {@render resolvedPopup()}\n  {:else}\n    <!-- Twitter/X style profile popup -->\n    <div class=\"space-y-3\">\n      <div class=\"flex items-start justify-between gap-3\">\n        <span class=\"ring-border/50 relative flex size-12 shrink-0 overflow-hidden rounded-full ring-2\">\n          {#if avatar}\n            <img src={avatar} alt={name || handle} class=\"aspect-square size-full object-cover\" />\n          {:else}\n            <span class=\"bg-muted flex size-full items-center justify-center text-sm font-semibold\">\n              {initials}\n            </span>\n          {/if}\n        </span>\n        <button\n          type=\"button\"\n          class={cn(\n            'h-8 rounded-full px-3.5 text-xs font-semibold transition-[transform,background-color] duration-150 active:scale-95',\n            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          onclick={(e) => {\n            e.stopPropagation()\n            isFollowing = !isFollowing\n          }}\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          {/if}\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        {/if}\n      </div>\n\n      {#if bio}\n        <p class=\"text-foreground/90 text-xs leading-relaxed\">{bio}</p>\n      {/if}\n\n      {#if joined}\n        <div class=\"text-muted-foreground flex items-center gap-4 text-xs\">\n          <div class=\"flex items-center gap-1\">\n            <Calendar class=\"size-3.5 opacity-70\" />\n            <span>Joined {joined}</span>\n          </div>\n        </div>\n      {/if}\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          {/if}\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          {/if}\n        </div>\n      {/if}\n    </div>\n  {/if}\n{/snippet}\n\n{#if popover}\n  <!-- Hand-rolled HoverCard: no headless float dependency in the Svelte\n       registry yet, so open/close delays + outside dismiss are local. -->\n  <!-- svelte-ignore a11y_no_static_element_interactions: hover bridge for the tag + card, not interactive itself -->\n  <span\n    class=\"relative inline-block\"\n    onmouseenter={scheduleOpen}\n    onmouseleave={scheduleClose}\n    onfocusin={scheduleOpen}\n    onfocusout={scheduleClose}\n  >\n    {#if href}\n      <a\n        bind:this={ref}\n        {href}\n        data-uipkge=\"\"\n        data-slot=\"mention-tag\"\n        class={tagClass}\n        {...(restProps as HTMLAnchorAttributes)}\n      >\n        {@render tagContent()}\n      </a>\n    {:else}\n      <span\n        bind:this={ref}\n        role=\"button\"\n        data-uipkge=\"\"\n        data-slot=\"mention-tag\"\n        class={tagClass}\n        tabindex=\"0\"\n        {...restProps}\n      >\n        {@render tagContent()}\n      </span>\n    {/if}\n    {#if cardOpen}\n      <!-- svelte-ignore a11y_no_static_element_interactions: hover bridge keeps the card open while hovered -->\n      <div\n        data-slot=\"mention-tag-content\"\n        class=\"bg-popover text-popover-foreground border-border/80 absolute top-full left-0 z-50 mt-1.5 w-80 rounded-xl border p-4 shadow-lg\"\n        onmouseenter={scheduleOpen}\n        onmouseleave={scheduleClose}\n      >\n        {@render profileCard()}\n      </div>\n    {/if}\n  </span>\n{:else}\n  <!-- Non-popover fallback -->\n  {#if href}\n    <a\n      bind:this={ref}\n      {href}\n      data-uipkge=\"\"\n      data-slot=\"mention-tag\"\n      class={plainTagClass}\n      {...(restProps as HTMLAnchorAttributes)}\n    >\n      {@render tagContent()}\n    </a>\n  {:else}\n    <span bind:this={ref} data-uipkge=\"\" data-slot=\"mention-tag\" class={plainTagClass} {...restProps}>\n      {@render tagContent()}\n    </span>\n  {/if}\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/mentions/MentionTag.svelte"
    },
    {
      "path": "packages/registry-svelte/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/lib/components/ui/mentions/caret-position.ts"
    },
    {
      "path": "packages/registry-svelte/components/mentions/index.ts",
      "content": "export 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]: any\n}\n\nexport { default as Mentions, type MentionsProps } from './Mentions.svelte'\nexport { default as MentionTag, type MentionTagProps } from './MentionTag.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/mentions/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Textarea with trigger-character autocomplete and companion MentionTag with Twitter/X-style hover profile card popup.",
  "categories": [
    "form"
  ]
}