{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"input-tag","type":"registry:ui","title":"InputTag","description":"InputTag from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":[],"files":[{"path":"components\/fancy\/input-tag\/InputTag.tsx","content":"import {\r\n  type CSSProperties,\r\n  type ReactNode,\r\n  useCallback,\r\n  useEffect,\r\n  useLayoutEffect,\r\n  useMemo,\r\n  useRef,\r\n  useState,\r\n} from \"react\";\r\n\r\n\/**\r\n * InputTag \u2014 attach a `\/`-style or `@`-style autocomplete picker to *any*\r\n * text surface (a `<textarea>`, an `<input>`, a `contenteditable`, a code\r\n * editor, a sheet cell editor, a whiteboard sticky note\u2026) via an adapter\r\n * that abstracts the surface's read\/write\/caret API.\r\n *\r\n * The component is \"headless\" in the sense that it has no input of its\r\n * own \u2014 it floats a small picker menu near the surface and writes back\r\n * through the adapter when the user picks an item.\r\n *\r\n *   <textarea ref={ref} value={text} onChange={...} \/>\r\n *   <InputTag\r\n *     adapter={textareaAdapter(ref)}\r\n *     triggers={{\r\n *       \"\/\": { items: commands, insert: (c) => `${c.name} ` },\r\n *       \"@\": { items: mentions, insert: (m) => `@${m.name} ` },\r\n *     }}\r\n *   \/>\r\n *\r\n * For non-DOM surfaces, write a small adapter that conforms to\r\n * {@link InputTagAdapter}. Adapters typically run 20\u201340 lines.\r\n *\/\r\n\r\n\/** What the surface tells the picker on every text\/caret change. *\/\r\nexport type InputTagAdapterState = {\r\n  text: string;\r\n  caretIndex: number;\r\n};\r\n\r\n\/**\r\n * The contract every input surface implements. The picker subscribes to\r\n * state changes, anchors itself using `getAnchorRect`, intercepts\r\n * navigation keys via `onKey`, and writes the selected item back through\r\n * `replaceRange`.\r\n *\/\r\nexport type InputTagAdapter = {\r\n  \/** Push current state whenever text or caret changes. Returns an unsubscribe. *\/\r\n  subscribe: (fn: (state: InputTagAdapterState) => void) => () => void;\r\n  \/** Replace text in `[start, end)` with `replacement`. The adapter is\r\n   *  responsible for moving the caret to the end of the inserted text. *\/\r\n  replaceRange: (start: number, end: number, replacement: string) => void;\r\n  \/** Screen-space rect to anchor the picker against. Usually the surface's\r\n   *  bounding rect; for editors that can compute caret-precise coordinates,\r\n   *  return a small rect there instead. *\/\r\n  getAnchorRect: () => DOMRect | null;\r\n  \/** Subscribe to keydown events on the surface. The handler may return\r\n   *  `true` to consume the event (the adapter then calls\r\n   *  `preventDefault` \/ `stopPropagation`). Returns an unsubscribe. *\/\r\n  onKey: (handler: (e: KeyboardEvent) => boolean) => () => void;\r\n};\r\n\r\n\/**\r\n * Per-trigger configuration. `T` is the item shape \u2014 typically a\r\n * `{ id, name, ... }` object, but any shape works as long as `keyOf` \/\r\n * `insert` know how to handle it.\r\n *\/\r\nexport type InputTagTrigger<T> = {\r\n  \/** The pool the picker filters against. *\/\r\n  items: T[];\r\n  \/** Text inserted in place of `<triggerChar><query>`. *\/\r\n  insert: (item: T, query: string) => string;\r\n  \/** Custom filter. Default: case-insensitive prefix match against\r\n   *  `String(keyOf(item))`. Pass `() => true` to disable filtering and\r\n   *  rely on `items` being pre-filtered (e.g. from an async source). *\/\r\n  filter?: (item: T, query: string) => boolean;\r\n  \/** How to render each row. Default renders the result of `keyOf(item)`. *\/\r\n  render?: (item: T, active: boolean) => ReactNode;\r\n  \/** How to derive a stable key + default label for each item. Default\r\n   *  reads `item.name` then `item.id` then `String(item)`. *\/\r\n  keyOf?: (item: T) => string;\r\n  \/** Optional header label shown above the list. *\/\r\n  label?: string;\r\n};\r\n\r\nexport type InputTagTriggers = Record<string, InputTagTrigger<any>>;\r\n\r\nexport interface InputTagProps {\r\n  adapter: InputTagAdapter;\r\n  triggers: InputTagTriggers;\r\n  \/** Max rows shown. Default 8. *\/\r\n  maxItems?: number;\r\n  \/** Where the picker anchors relative to the surface. Default `\"bottom-left\"`. *\/\r\n  placement?: \"bottom-left\" | \"bottom-right\" | \"top-left\" | \"top-right\";\r\n  \/** Custom class for the popover container. *\/\r\n  className?: string;\r\n  \/** Inline style for the popover container. *\/\r\n  style?: CSSProperties;\r\n  \/** Fires whenever an item is picked. *\/\r\n  onPick?: (info: { triggerChar: string; query: string; item: unknown }) => void;\r\n};\r\n\r\ntype ActiveTrigger = {\r\n  char: string;\r\n  start: number;\r\n  end: number;\r\n  query: string;\r\n};\r\n\r\nfunction defaultKeyOf(item: unknown): string {\r\n  if (item && typeof item === \"object\") {\r\n    const r = item as Record<string, unknown>;\r\n    if (typeof r.name === \"string\") return r.name;\r\n    if (typeof r.id === \"string\") return r.id;\r\n  }\r\n  return String(item);\r\n}\r\n\r\nfunction defaultFilter(item: unknown, query: string): boolean {\r\n  if (!query) return true;\r\n  return defaultKeyOf(item).toLowerCase().includes(query.toLowerCase());\r\n}\r\n\r\n\/**\r\n * Walk back from `caret` to detect an active trigger. A trigger is active\r\n * when one of the configured characters appears between `caret` and the\r\n * nearest preceding whitespace (or start-of-text), with no other\r\n * non-trigger-non-whitespace breaks. The \"query\" is everything after\r\n * the trigger up to the caret.\r\n *\/\r\nfunction detectTrigger(\r\n  text: string,\r\n  caret: number,\r\n  triggerChars: Set<string>,\r\n): ActiveTrigger | null {\r\n  for (let i = caret - 1; i >= 0; i--) {\r\n    const ch = text[i];\r\n    if (ch === undefined) break;\r\n    if (triggerChars.has(ch)) {\r\n      \/\/ Must be at a word boundary: start of text OR preceded by whitespace.\r\n      if (i === 0 || \/\\s\/.test(text[i - 1])) {\r\n        return {\r\n          char: ch,\r\n          start: i,\r\n          end: caret,\r\n          query: text.slice(i + 1, caret),\r\n        };\r\n      }\r\n      return null;\r\n    }\r\n    if (\/\\s\/.test(ch)) return null;\r\n  }\r\n  return null;\r\n}\r\n\r\nexport function InputTag({\r\n  adapter,\r\n  triggers,\r\n  maxItems = 8,\r\n  placement = \"bottom-left\",\r\n  className,\r\n  style,\r\n  onPick,\r\n}: InputTagProps) {\r\n  const triggerChars = useMemo(() => new Set(Object.keys(triggers)), [triggers]);\r\n  const [active, setActive] = useState<ActiveTrigger | null>(null);\r\n  const [cursor, setCursor] = useState(0);\r\n  const [anchorRect, setAnchorRect] = useState<DOMRect | null>(null);\r\n\r\n  \/\/ Latest state in refs so key handlers see fresh data without re-registering.\r\n  const stateRef = useRef<InputTagAdapterState>({ text: \"\", caretIndex: 0 });\r\n  const activeRef = useRef<ActiveTrigger | null>(null);\r\n  const cursorRef = useRef(0);\r\n  activeRef.current = active;\r\n  cursorRef.current = cursor;\r\n\r\n  \/\/ Filtered items for the active trigger.\r\n  const items: unknown[] = useMemo(() => {\r\n    if (!active) return [];\r\n    const cfg = triggers[active.char];\r\n    if (!cfg) return [];\r\n    const filter = cfg.filter ?? defaultFilter;\r\n    const out: unknown[] = [];\r\n    for (const item of cfg.items) {\r\n      if (filter(item, active.query)) out.push(item);\r\n      if (out.length >= maxItems) break;\r\n    }\r\n    return out;\r\n  }, [active, triggers, maxItems]);\r\n\r\n  \/\/ Subscribe to text\/caret changes.\r\n  useEffect(() => {\r\n    return adapter.subscribe((s) => {\r\n      stateRef.current = s;\r\n      const next = detectTrigger(s.text, s.caretIndex, triggerChars);\r\n      setActive((prev) => {\r\n        \/\/ Reset cursor when the trigger char or position changes.\r\n        if (!prev || !next || prev.char !== next.char || prev.start !== next.start) {\r\n          setCursor(0);\r\n        }\r\n        return next;\r\n      });\r\n      if (next) setAnchorRect(adapter.getAnchorRect());\r\n    });\r\n  }, [adapter, triggerChars]);\r\n\r\n  \/\/ Refresh anchor rect on scroll\/resize while picker is open.\r\n  useLayoutEffect(() => {\r\n    if (!active) return;\r\n    const update = () => setAnchorRect(adapter.getAnchorRect());\r\n    update();\r\n    window.addEventListener(\"scroll\", update, true);\r\n    window.addEventListener(\"resize\", update);\r\n    return () => {\r\n      window.removeEventListener(\"scroll\", update, true);\r\n      window.removeEventListener(\"resize\", update);\r\n    };\r\n  }, [active, adapter]);\r\n\r\n  \/\/ Clamp cursor when the visible items shrink.\r\n  useEffect(() => {\r\n    if (cursor >= items.length) setCursor(Math.max(0, items.length - 1));\r\n  }, [items.length, cursor]);\r\n\r\n  const pickItem = useCallback(\r\n    (item: unknown) => {\r\n      const cur = activeRef.current;\r\n      if (!cur) return;\r\n      const cfg = triggers[cur.char];\r\n      if (!cfg) return;\r\n      const replacement = cfg.insert(item, cur.query);\r\n      adapter.replaceRange(cur.start, cur.end, replacement);\r\n      onPick?.({ triggerChar: cur.char, query: cur.query, item });\r\n      setActive(null);\r\n    },\r\n    [adapter, triggers, onPick],\r\n  );\r\n\r\n  \/\/ Key handling.\r\n  useEffect(() => {\r\n    return adapter.onKey((e) => {\r\n      const cur = activeRef.current;\r\n      if (!cur) return false;\r\n      const visible = items;\r\n      switch (e.key) {\r\n        case \"ArrowDown\":\r\n          if (visible.length === 0) return false;\r\n          setCursor((c) => (c + 1) % visible.length);\r\n          return true;\r\n        case \"ArrowUp\":\r\n          if (visible.length === 0) return false;\r\n          setCursor((c) => (c - 1 + visible.length) % visible.length);\r\n          return true;\r\n        case \"Enter\":\r\n        case \"Tab\": {\r\n          if (visible.length === 0) return false;\r\n          const idx = Math.min(cursorRef.current, visible.length - 1);\r\n          pickItem(visible[idx]);\r\n          return true;\r\n        }\r\n        case \"Escape\":\r\n          setActive(null);\r\n          return true;\r\n        default:\r\n          return false;\r\n      }\r\n    });\r\n  }, [adapter, items, pickItem]);\r\n\r\n  if (!active || !anchorRect || items.length === 0) return null;\r\n\r\n  const cfg = triggers[active.char];\r\n  if (!cfg) return null;\r\n\r\n  const popStyle = computePopoverStyle(anchorRect, placement);\r\n\r\n  return (\r\n    <div\r\n      role=\"listbox\"\r\n      aria-label={cfg.label ?? `Pick a ${active.char === \"\/\" ? \"command\" : \"mention\"}`}\r\n      className={[\r\n        \"fixed z-50 w-72 overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900\",\r\n        className ?? \"\",\r\n      ]\r\n        .filter(Boolean)\r\n        .join(\" \")}\r\n      style={{ ...popStyle, ...style }}\r\n    >\r\n      {(cfg.label || active.query) && (\r\n        <div className=\"flex items-center justify-between border-b border-zinc-100 bg-zinc-50 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950\">\r\n          <span>{cfg.label ?? labelForChar(active.char)}<\/span>\r\n          {active.query && (\r\n            <span className=\"font-mono text-[10px] normal-case tracking-normal text-zinc-400\">\r\n              {active.char}\r\n              {active.query}\r\n            <\/span>\r\n          )}\r\n        <\/div>\r\n      )}\r\n      <ul className=\"max-h-60 overflow-y-auto\">\r\n        {items.map((item, i) => {\r\n          const renderer = cfg.render ?? ((it: unknown) => <span>{defaultKeyOf(it)}<\/span>);\r\n          const active2 = i === cursor;\r\n          const key = (cfg.keyOf ?? defaultKeyOf)(item);\r\n          return (\r\n            <li\r\n              key={key}\r\n              role=\"option\"\r\n              aria-selected={active2}\r\n              onMouseDown={(e) => {\r\n                e.preventDefault();\r\n                pickItem(item);\r\n              }}\r\n              onMouseEnter={() => setCursor(i)}\r\n              className={[\r\n                \"cursor-pointer px-2 py-1.5 text-[12px]\",\r\n                active2\r\n                  ? \"bg-violet-100 dark:bg-violet-900\/30\"\r\n                  : \"hover:bg-zinc-50 dark:hover:bg-zinc-800\",\r\n              ].join(\" \")}\r\n            >\r\n              {renderer(item, active2)}\r\n            <\/li>\r\n          );\r\n        })}\r\n      <\/ul>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction labelForChar(ch: string): string {\r\n  if (ch === \"\/\") return \"Commands\";\r\n  if (ch === \"@\") return \"Mentions\";\r\n  if (ch === \"#\") return \"Tags\";\r\n  if (ch === \":\") return \"Emoji\";\r\n  return `Trigger ${ch}`;\r\n}\r\n\r\nfunction computePopoverStyle(\r\n  anchor: DOMRect,\r\n  placement: NonNullable<InputTagProps[\"placement\"]>,\r\n): CSSProperties {\r\n  const offset = 4;\r\n  switch (placement) {\r\n    case \"bottom-right\":\r\n      return { top: anchor.bottom + offset, right: window.innerWidth - anchor.right };\r\n    case \"top-left\":\r\n      return { bottom: window.innerHeight - anchor.top + offset, left: anchor.left };\r\n    case \"top-right\":\r\n      return { bottom: window.innerHeight - anchor.top + offset, right: window.innerWidth - anchor.right };\r\n    case \"bottom-left\":\r\n    default:\r\n      return { top: anchor.bottom + offset, left: anchor.left };\r\n  }\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/input-tag\/InputTag.tsx"},{"path":"components\/fancy\/input-tag\/adapters.ts","content":"import type { RefObject } from \"react\";\r\nimport type { InputTagAdapter } from \".\/InputTag\";\r\n\r\n\/**\r\n * Built-in DOM adapters. Each one wraps a familiar input element and\r\n * exposes the {@link InputTagAdapter} contract so `<InputTag>` can\r\n * attach to it. For non-DOM surfaces (code editor, sheet cell editor,\r\n * whiteboard sticky note), write your own \u2014 the contract is small.\r\n *\/\r\n\r\n\/**\r\n * Get React's monkey-patched-aside native value setter on a DOM\r\n * element. Required when programmatically updating `value` on a\r\n * controlled input: React's intercepted setter would otherwise swallow\r\n * the change and the consumer's `onChange` would never fire.\r\n *\/\r\nfunction nativeValueSetter(el: HTMLInputElement | HTMLTextAreaElement): ((v: string) => void) | null {\r\n  const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;\r\n  const desc = Object.getOwnPropertyDescriptor(proto, \"value\");\r\n  if (!desc?.set) return null;\r\n  return desc.set.bind(el);\r\n}\r\n\r\nfunction replaceInValueElement(\r\n  el: HTMLInputElement | HTMLTextAreaElement,\r\n  start: number,\r\n  end: number,\r\n  replacement: string,\r\n): void {\r\n  const current = el.value;\r\n  const next = current.slice(0, start) + replacement + current.slice(end);\r\n  const setter = nativeValueSetter(el);\r\n  if (setter) {\r\n    setter(next);\r\n  } else {\r\n    el.value = next;\r\n  }\r\n  \/\/ Dispatch a native `input` event so any controlled-component\r\n  \/\/ onChange handler upstream gets the new value.\r\n  el.dispatchEvent(new Event(\"input\", { bubbles: true }));\r\n  \/\/ Place caret at the end of the inserted text.\r\n  const caret = start + replacement.length;\r\n  el.setSelectionRange(caret, caret);\r\n  el.focus();\r\n}\r\n\r\nfunction valueElementAdapter(\r\n  ref: RefObject<HTMLTextAreaElement | HTMLInputElement | null>,\r\n): InputTagAdapter {\r\n  return {\r\n    subscribe(fn) {\r\n      const handler = () => {\r\n        const el = ref.current;\r\n        if (!el) return;\r\n        fn({\r\n          text: el.value,\r\n          caretIndex: el.selectionStart ?? el.value.length,\r\n        });\r\n      };\r\n      const el = ref.current;\r\n      if (el) {\r\n        el.addEventListener(\"input\", handler);\r\n        el.addEventListener(\"click\", handler);\r\n        el.addEventListener(\"keyup\", handler);\r\n        el.addEventListener(\"focus\", handler);\r\n      }\r\n      return () => {\r\n        const el2 = ref.current;\r\n        if (el2) {\r\n          el2.removeEventListener(\"input\", handler);\r\n          el2.removeEventListener(\"click\", handler);\r\n          el2.removeEventListener(\"keyup\", handler);\r\n          el2.removeEventListener(\"focus\", handler);\r\n        }\r\n      };\r\n    },\r\n    replaceRange(start, end, replacement) {\r\n      const el = ref.current;\r\n      if (!el) return;\r\n      replaceInValueElement(el, start, end, replacement);\r\n    },\r\n    getAnchorRect() {\r\n      const el = ref.current;\r\n      return el ? el.getBoundingClientRect() : null;\r\n    },\r\n    onKey(handler) {\r\n      const fn = (e: KeyboardEvent) => {\r\n        if (handler(e)) {\r\n          e.preventDefault();\r\n          e.stopPropagation();\r\n        }\r\n      };\r\n      const el = ref.current;\r\n      if (el) el.addEventListener(\"keydown\", fn as EventListener);\r\n      return () => {\r\n        const el2 = ref.current;\r\n        if (el2) el2.removeEventListener(\"keydown\", fn as EventListener);\r\n      };\r\n    },\r\n  };\r\n}\r\n\r\n\/**\r\n * Adapter for a DOM `<textarea>`. Works whether the consumer manages\r\n * `value` via React state (controlled) or lets the DOM hold it\r\n * (uncontrolled) \u2014 `replaceRange` uses React's native value setter and\r\n * dispatches a native `input` event so the upstream onChange fires.\r\n *\/\r\nexport function textareaAdapter(\r\n  ref: RefObject<HTMLTextAreaElement | null>,\r\n): InputTagAdapter {\r\n  return valueElementAdapter(ref);\r\n}\r\n\r\n\/** Adapter for a DOM `<input type=\"text|search|...\">`. *\/\r\nexport function inputAdapter(\r\n  ref: RefObject<HTMLInputElement | null>,\r\n): InputTagAdapter {\r\n  return valueElementAdapter(ref);\r\n}\r\n\r\n\/**\r\n * Adapter for any `contenteditable` element. Operates on the element's\r\n * `textContent`; rich-text editors with their own internal model should\r\n * write a custom adapter that talks to that model instead.\r\n *\/\r\nexport function contentEditableAdapter(\r\n  ref: RefObject<HTMLElement | null>,\r\n): InputTagAdapter {\r\n  function caretIndex(el: HTMLElement): number {\r\n    const sel = window.getSelection();\r\n    if (!sel || sel.rangeCount === 0) return 0;\r\n    const range = sel.getRangeAt(0).cloneRange();\r\n    range.selectNodeContents(el);\r\n    range.setEnd(sel.focusNode!, sel.focusOffset);\r\n    return range.toString().length;\r\n  }\r\n  function setCaret(el: HTMLElement, index: number): void {\r\n    const sel = window.getSelection();\r\n    if (!sel) return;\r\n    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);\r\n    let remaining = index;\r\n    let node: Node | null = walker.nextNode();\r\n    while (node) {\r\n      const len = (node.textContent ?? \"\").length;\r\n      if (remaining <= len) {\r\n        const range = document.createRange();\r\n        range.setStart(node, remaining);\r\n        range.collapse(true);\r\n        sel.removeAllRanges();\r\n        sel.addRange(range);\r\n        return;\r\n      }\r\n      remaining -= len;\r\n      node = walker.nextNode();\r\n    }\r\n    \/\/ Past end: place at end.\r\n    const range = document.createRange();\r\n    range.selectNodeContents(el);\r\n    range.collapse(false);\r\n    sel.removeAllRanges();\r\n    sel.addRange(range);\r\n  }\r\n  return {\r\n    subscribe(fn) {\r\n      const handler = () => {\r\n        const el = ref.current;\r\n        if (!el) return;\r\n        fn({ text: el.textContent ?? \"\", caretIndex: caretIndex(el) });\r\n      };\r\n      const el = ref.current;\r\n      if (el) {\r\n        el.addEventListener(\"input\", handler);\r\n        el.addEventListener(\"click\", handler);\r\n        el.addEventListener(\"keyup\", handler);\r\n        el.addEventListener(\"focus\", handler);\r\n      }\r\n      return () => {\r\n        const el2 = ref.current;\r\n        if (el2) {\r\n          el2.removeEventListener(\"input\", handler);\r\n          el2.removeEventListener(\"click\", handler);\r\n          el2.removeEventListener(\"keyup\", handler);\r\n          el2.removeEventListener(\"focus\", handler);\r\n        }\r\n      };\r\n    },\r\n    replaceRange(start, end, replacement) {\r\n      const el = ref.current;\r\n      if (!el) return;\r\n      const current = el.textContent ?? \"\";\r\n      const next = current.slice(0, start) + replacement + current.slice(end);\r\n      el.textContent = next;\r\n      setCaret(el, start + replacement.length);\r\n      el.dispatchEvent(new Event(\"input\", { bubbles: true }));\r\n      el.focus();\r\n    },\r\n    getAnchorRect() {\r\n      const el = ref.current;\r\n      return el ? el.getBoundingClientRect() : null;\r\n    },\r\n    onKey(handler) {\r\n      const fn = (e: KeyboardEvent) => {\r\n        if (handler(e)) {\r\n          e.preventDefault();\r\n          e.stopPropagation();\r\n        }\r\n      };\r\n      const el = ref.current;\r\n      if (el) el.addEventListener(\"keydown\", fn as EventListener);\r\n      return () => {\r\n        const el2 = ref.current;\r\n        if (el2) el2.removeEventListener(\"keydown\", fn as EventListener);\r\n      };\r\n    },\r\n  };\r\n}\r\n\r\n\/**\r\n * Adapter for hosts that fully control the text state themselves. Use\r\n * this when you can't (or don't want to) let the DOM be the source of\r\n * truth \u2014 for code editors with internal state, sheet cell editors, or\r\n * any host that already has `value` + `onChange` and wants to reach the\r\n * picker programmatically.\r\n *\r\n * The host calls `notify({ text, caretIndex })` after every text or\r\n * caret change. `<InputTag>` calls `onReplaceRange` to write back.\r\n *\/\r\nexport type ControlledAdapterHandle = InputTagAdapter & {\r\n  \/** Push the latest text + caret to the picker. *\/\r\n  notify: (state: { text: string; caretIndex: number }) => void;\r\n};\r\n\r\nexport function controlledAdapter(opts: {\r\n  \/** Element used as anchor + key target. Usually the visible input. *\/\r\n  anchorRef: RefObject<HTMLElement | null>;\r\n  \/** Called when the picker writes back an insertion. *\/\r\n  onReplaceRange: (start: number, end: number, replacement: string) => void;\r\n}): ControlledAdapterHandle {\r\n  let listener: ((s: { text: string; caretIndex: number }) => void) | null = null;\r\n  let keyHandler: ((e: KeyboardEvent) => boolean) | null = null;\r\n\r\n  const installKey = () => {\r\n    const el = opts.anchorRef.current;\r\n    if (!el || !keyHandler) return () => {};\r\n    const fn = (e: KeyboardEvent) => {\r\n      if (keyHandler && keyHandler(e)) {\r\n        e.preventDefault();\r\n        e.stopPropagation();\r\n      }\r\n    };\r\n    el.addEventListener(\"keydown\", fn);\r\n    return () => el.removeEventListener(\"keydown\", fn);\r\n  };\r\n\r\n  return {\r\n    notify(state) {\r\n      listener?.(state);\r\n    },\r\n    subscribe(fn) {\r\n      listener = fn;\r\n      return () => {\r\n        if (listener === fn) listener = null;\r\n      };\r\n    },\r\n    replaceRange(start, end, replacement) {\r\n      opts.onReplaceRange(start, end, replacement);\r\n    },\r\n    getAnchorRect() {\r\n      const el = opts.anchorRef.current;\r\n      return el ? el.getBoundingClientRect() : null;\r\n    },\r\n    onKey(handler) {\r\n      keyHandler = handler;\r\n      const uninstall = installKey();\r\n      return () => {\r\n        if (keyHandler === handler) keyHandler = null;\r\n        uninstall();\r\n      };\r\n    },\r\n  };\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/input-tag\/adapters.ts"},{"path":"components\/fancy\/input-tag\/index.ts","content":"export { InputTag } from \".\/InputTag\";\r\nexport type {\r\n  InputTagProps,\r\n  InputTagAdapter,\r\n  InputTagAdapterState,\r\n  InputTagTrigger,\r\n  InputTagTriggers,\r\n} from \".\/InputTag\";\r\nexport {\r\n  textareaAdapter,\r\n  inputAdapter,\r\n  contentEditableAdapter,\r\n  controlledAdapter,\r\n  type ControlledAdapterHandle,\r\n} from \".\/adapters\";\r\n","type":"registry:ui","target":"components\/fancy\/input-tag\/index.ts"}]}