{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mention-textarea",
  "title": "Mention Textarea",
  "description": "A textarea that suggests people, channels or anything else after a trigger character.",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "popover",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/mention-textarea.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { flushSync } from \"react-dom\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport {\n  filterMentionOptions,\n  findActiveMention,\n  insertMention,\n  type MentionOption,\n} from \"@/registry/default/lib/mention\"\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n} from \"@/registry/default/ui/popover\"\n\nfunction MentionTextarea({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  triggers,\n  onKeyDown,\n  className,\n  ...props\n}: Omit<\n  React.ComponentProps<\"textarea\">,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\"\n> & {\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  triggers: Record<string, MentionOption[]>\n}) {\n  const listId = React.useId()\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n  const { value: text, setValue: setText } = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  })\n  const [caret, setCaret] = React.useState(0)\n  const [dismissedStart, setDismissedStart] = React.useState<number | null>(\n    null\n  )\n  const [highlightedIndex, setHighlightedIndex] = React.useState(0)\n\n  const mention = findActiveMention(text, caret, Object.keys(triggers))\n  const options = mention\n    ? filterMentionOptions(triggers[mention.trigger], mention.query)\n    : []\n  const isOpen =\n    mention !== null && options.length > 0 && mention.start !== dismissedStart\n  const mentionKey = mention ? `${mention.start}:${mention.query}` : \"\"\n  const [previousMentionKey, setPreviousMentionKey] = React.useState(mentionKey)\n\n  if (mentionKey !== previousMentionKey) {\n    setPreviousMentionKey(mentionKey)\n    setHighlightedIndex(0)\n  }\n\n  function selectOption(option: MentionOption) {\n    if (!mention) return\n    const next = insertMention(text, mention, caret, option)\n    flushSync(() => {\n      setText(next.text)\n      setCaret(next.caret)\n    })\n    textareaRef.current?.setSelectionRange(next.caret, next.caret)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {\n    onKeyDown?.(event)\n    if (!isOpen || event.defaultPrevented) return\n\n    switch (event.key) {\n      case \"ArrowDown\":\n      case \"ArrowUp\": {\n        event.preventDefault()\n        const delta = event.key === \"ArrowDown\" ? 1 : -1\n        setHighlightedIndex(\n          (highlightedIndex + delta + options.length) % options.length\n        )\n        return\n      }\n      case \"Enter\":\n      case \"Tab\":\n        event.preventDefault()\n        selectOption(options[highlightedIndex])\n        return\n      case \"Escape\":\n        event.preventDefault()\n        setDismissedStart(mention.start)\n        return\n    }\n  }\n\n  return (\n    <Popover\n      open={isOpen}\n      onOpenChange={(open) => {\n        if (!open && mention) setDismissedStart(mention.start)\n      }}\n    >\n      <PopoverAnchor asChild>\n        <textarea\n          ref={textareaRef}\n          data-slot=\"mention-textarea\"\n          role=\"combobox\"\n          aria-autocomplete=\"list\"\n          aria-expanded={isOpen}\n          aria-controls={isOpen ? listId : undefined}\n          aria-activedescendant={\n            isOpen ? `${listId}-${highlightedIndex}` : undefined\n          }\n          value={text}\n          onChange={(event) => {\n            setText(event.target.value)\n            setCaret(event.target.selectionStart)\n          }}\n          onSelect={(event) => setCaret(event.currentTarget.selectionStart)}\n          onKeyDown={handleKeyDown}\n          className={cn(\n            \"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n            \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n            className\n          )}\n          {...props}\n        />\n      </PopoverAnchor>\n      <PopoverContent\n        align=\"start\"\n        onOpenAutoFocus={(event) => event.preventDefault()}\n        onCloseAutoFocus={(event) => event.preventDefault()}\n        onInteractOutside={(event) => {\n          if (event.target === textareaRef.current) event.preventDefault()\n        }}\n        className=\"w-64 p-1\"\n      >\n        <ul id={listId} role=\"listbox\" data-slot=\"mention-textarea-list\">\n          {options.map((option, index) => (\n            <li\n              key={option.value}\n              id={`${listId}-${index}`}\n              role=\"option\"\n              aria-selected={index === highlightedIndex}\n              onPointerDown={(event) => event.preventDefault()}\n              onPointerMove={() => setHighlightedIndex(index)}\n              onClick={() => selectOption(option)}\n              className=\"flex cursor-default flex-col rounded-sm px-2 py-1.5 text-sm aria-selected:bg-accent aria-selected:text-accent-foreground\"\n            >\n              <span>{option.label}</span>\n              {option.description && (\n                <span className=\"text-xs text-muted-foreground\">\n                  {option.description}\n                </span>\n              )}\n            </li>\n          ))}\n        </ul>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport { MentionTextarea }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/mention.ts",
      "content": "export type MentionOption = {\n  value: string\n  label: string\n  description?: string\n}\n\nexport type ActiveMention = {\n  trigger: string\n  query: string\n  start: number\n}\n\nconst TRAILING_TOKEN = /(^|\\s)(\\S)(\\S*)$/\nconst MAX_VISIBLE_OPTIONS = 8\n\nexport function findActiveMention(\n  text: string,\n  caret: number,\n  triggers: string[]\n): ActiveMention | null {\n  const match = TRAILING_TOKEN.exec(text.slice(0, caret))\n  if (!match) return null\n  const [, , trigger, query] = match\n  if (!triggers.includes(trigger)) return null\n  return { trigger, query, start: caret - query.length - 1 }\n}\n\nexport function filterMentionOptions(options: MentionOption[], query: string) {\n  const needle = query.toLowerCase()\n  return options\n    .filter(\n      (option) =>\n        option.value.toLowerCase().includes(needle) ||\n        option.label.toLowerCase().includes(needle)\n    )\n    .slice(0, MAX_VISIBLE_OPTIONS)\n}\n\nexport function insertMention(\n  text: string,\n  mention: ActiveMention,\n  caret: number,\n  option: MentionOption\n) {\n  const inserted = `${mention.trigger}${option.value} `\n  return {\n    text: text.slice(0, mention.start) + inserted + text.slice(caret),\n    caret: mention.start + inserted.length,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}