{
  "$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": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "utils",
    "popover",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/base/ui/mention-textarea.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\"\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\"\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    <PopoverPrimitive.Root\n      open={isOpen}\n      onOpenChange={(open, { event }) => {\n        if (open || !mention || event.target === textareaRef.current) return\n        setDismissedStart(mention.start)\n      }}\n    >\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      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Positioner\n          anchor={textareaRef}\n          align=\"start\"\n          sideOffset={4}\n          className=\"isolate z-50\"\n        >\n          <PopoverPrimitive.Popup\n            initialFocus={false}\n            finalFocus={false}\n            className=\"z-50 w-64 origin-(--transform-origin) rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95\"\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          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\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"
}