{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-mention-input",
  "type": "registry:component",
  "title": "Mention input",
  "description": "`@name` autocompletion in a text field",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/internal/controllable.ts",
      "target": "@ui/gpuixui/components/internal/controllable.ts",
      "type": "registry:lib",
      "content": "import { useCallback, useState } from \"react\";\n\nexport type ControllableState<Value> = readonly [Value, (next: Value) => void];\n\nexport interface ControllableStateOptions<Value> {\n  readonly value: Value | undefined;\n\n  readonly defaultValue: Value;\n\n  readonly onChange?: (next: Value) => void;\n}\n\nexport const useControllableState = <Value>({\n  value,\n  defaultValue,\n  onChange,\n}: ControllableStateOptions<Value>): ControllableState<Value> => {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n\n  const setValue = useCallback(\n    (next: Value) => {\n      if (!controlled) setInternal(next);\n      if (value !== next) onChange?.(next);\n    },\n    [controlled, value, onChange],\n  );\n\n  return [controlled ? value : internal, setValue];\n};\n"
    },
    {
      "path": "packages/components/src/internal/key.ts",
      "target": "@ui/gpuixui/components/internal/key.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, KeyEvent, Props } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport type KeyPhase = \"down\" | \"up\";\n\nexport const isActivation = (event: EventPayload, phase: KeyPhase): boolean => {\n  if (event.isHeld === true) return false;\n  if (event.key === \"enter\") return phase === \"down\";\n  return event.key === \"space\" && phase === \"up\";\n};\n\nexport const swallowsDefault = (\n  event: EventPayload,\n  phase: KeyPhase,\n): boolean => event.key === \"space\" && phase === \"down\";\n\nexport const activationHandlers = (\n  activate: (event: KeyEvent) => void,\n  observe: Pick<Props, \"onKeyDown\" | \"onKeyUp\"> = {},\n): Pick<Props, \"onKeyDown\" | \"onKeyUp\"> => ({\n  onKeyDown: (event: KeyEvent) => {\n    if (swallowsDefault(event, \"down\")) event.preventDefault();\n    if (isActivation(event, \"down\")) activate(event);\n    observe.onKeyDown?.(event);\n  },\n  onKeyUp: (event: KeyEvent) => {\n    if (isActivation(event, \"up\")) activate(event);\n    observe.onKeyUp?.(event);\n  },\n});\n\nexport const isEnter = (event: EventPayload): boolean => event.key === \"enter\";\n\nexport const isDown = (event: EventPayload): boolean =>\n  event.key === \"down\" || (event.key === \"n\" && event.modifiers?.ctrl === true);\n\nexport const isUp = (event: EventPayload): boolean =>\n  event.key === \"up\" || (event.key === \"p\" && event.modifiers?.ctrl === true);\n\nexport const isRight = (event: EventPayload): boolean => event.key === \"right\";\n\nexport const isLeft = (event: EventPayload): boolean => event.key === \"left\";\n\nexport const isFirst = (event: EventPayload): boolean => event.key === \"home\";\n\nexport const isLast = (event: EventPayload): boolean => event.key === \"end\";\n\nexport const isPageUp = (event: EventPayload): boolean =>\n  event.key === \"pageup\";\n\nexport const isPageDown = (event: EventPayload): boolean =>\n  event.key === \"pagedown\";\n\nexport const isDismiss = (event: EventPayload): boolean =>\n  event.key === \"escape\";\n\nexport const isTab = (event: EventPayload): boolean => event.key === \"tab\";\n\nexport const isSelectAll = (event: EventPayload): boolean =>\n  event.key === \"a\" &&\n  (event.modifiers?.cmd === true || event.modifiers?.ctrl === true);\n\nexport const isMenuKey = (event: EventPayload): boolean =>\n  (event.key === \"f10\" && event.modifiers?.shift === true) ||\n  event.key === \"contextmenu\";\n\nexport const axisDirection = (\n  event: EventPayload,\n  orientation: \"horizontal\" | \"vertical\",\n): Option.Option<1 | -1> => {\n  if (event.key === \"right\") return Option.some(1);\n  if (event.key === \"left\") return Option.some(-1);\n  if (orientation === \"vertical\") {\n    if (isDown(event)) return Option.some(1);\n    if (isUp(event)) return Option.some(-1);\n  }\n  return Option.none();\n};\n"
    },
    {
      "path": "packages/components/src/internal/parts.ts",
      "target": "@ui/gpuixui/components/internal/parts.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport type PartComponent = (props: never) => ReactNode;\n\nexport type PartChildren =\n  | ReactElement<unknown>\n  | boolean\n  | null\n  | undefined\n  | ReadonlyArray<PartChildren>;\n\nexport interface PartsContract {\n  readonly parent: string;\n\n  readonly family: ReadonlyArray<PartComponent>;\n\n  readonly accepts: ReadonlyArray<PartComponent>;\n\n  readonly hint?: string;\n}\n\nconst componentName = (type: unknown): string =>\n  P.isString(type)\n    ? type\n    : P.isFunction(type)\n      ? type.name || \"anonymous\"\n      : \"element\";\n\nconst fragmentChildren = (element: ReactElement<unknown>): ReactNode =>\n  hasChildren(element.props) ? element.props.children : undefined;\n\nconst hasChildren = (\n  value: unknown,\n): value is { readonly children?: ReactNode } =>\n  P.isObject(value) && \"children\" in value;\n\nconst leaves = (children: ReactNode): ReadonlyArray<unknown> =>\n  Arr.flatMap(Children.toArray(children), (child) =>\n    isValidElement<unknown>(child) && child.type === Fragment\n      ? leaves(fragmentChildren(child))\n      : [child],\n  );\n\nexport const assertParts = (\n  contract: PartsContract,\n  children: ReactNode,\n): void => {\n  Arr.forEach(leaves(children), (leaf) => {\n    if (!isValidElement(leaf)) return;\n    if (Arr.some(contract.accepts, (part) => part === leaf.type)) return;\n    if (!Arr.some(contract.family, (part) => part === leaf.type)) return;\n\n    throw new Error(\n      `${componentName(leaf.type)} cannot be used inside ${contract.parent}.` +\n        (contract.hint === undefined ? \"\" : ` ${contract.hint}`),\n    );\n  });\n};\n"
    },
    {
      "path": "packages/components/src/internal/presence.tsx",
      "target": "@ui/gpuixui/components/internal/presence.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { MotionProps } from \"@gpuix/react\";\nimport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceMotion as motionOfPresence,\n  presenceStyle,\n  transitionWindowMs,\n  type PresenceFrames,\n  type PresenceHitRule,\n  type PresenceSide,\n  type PresenceTiming,\n  type PresenceTransition,\n} from \"../../motion/presence.js\";\nimport {\n  MotionProvider,\n  useMotionPreference,\n  useSurfacePresence,\n  useSurfaceTiming,\n  type MotionProviderProps,\n  type SurfacePresence,\n} from \"../../motion/react/presence.js\";\nimport {\n  motionTiming,\n  type MotionLevel,\n  type MotionPreference,\n} from \"../../motion/timing.js\";\n\nexport { MotionProvider, useMotionPreference, type MotionProviderProps };\n\nexport type SurfaceFrames = PresenceFrames;\n\nexport type SurfaceTiming = PresenceTiming;\n\nexport type PanelSide = PresenceSide;\n\nexport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceStyle,\n  transitionWindowMs,\n};\nexport type { PresenceHitRule };\n\nexport { useSurfacePresence, useSurfaceTiming, type SurfacePresence };\n\nexport const moveTiming = (\n  level: MotionLevel,\n  preference: MotionPreference,\n): PresenceTransition => motionTiming(level, { phase: \"standard\", preference });\n\nexport const useMoveTiming = (level: MotionLevel): PresenceTransition =>\n  moveTiming(level, useMotionPreference());\n\nexport const presenceMotion = (\n  frames: SurfaceFrames,\n  presence: SurfacePresence,\n  timing: SurfaceTiming = presence.timing,\n): MotionProps => motionOfPresence(frames, presence.present, timing);\n\nexport const PresenceShield = ({ present }: { readonly present: boolean }) =>\n  present ? null : (\n    <div\n      testId=\"presence-shield\"\n      style={{ position: \"absolute\", top: 0, right: 0, bottom: 0, left: 0 }}\n    />\n  );\n\nexport const presenceTabStop = (\n  present: boolean,\n): { readonly tabIndex?: number } => (present ? {} : { tabIndex: -1 });\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/mention-input.tsx",
      "target": "@ui/gpuixui/components/mention-input.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload, InputProps, Props } from \"@gpuix/react\";\nimport {\n  FloatingLayer,\n  floatingRootStyle,\n  mergeStyles,\n} from \"@gpuix/react/floating\";\nimport {\n  cn,\n  MENTION_INPUT_CONTROL,\n  MENTION_INPUT_FIELD,\n  MENTION_INPUT_ITEM,\n  MENTION_INPUT_ITEM_TEXT,\n  MENTION_INPUT_LIST,\n} from \"../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactElement,\n} from \"react\";\nimport { useControllableState } from \"./internal/controllable.js\";\nimport {\n  fieldHelp,\n  fieldHint,\n  hintHost,\n  renderFieldHint,\n} from \"../primitives/components/hint.js\";\nimport { activationHandlers, isDismiss, isDown, isUp } from \"./internal/key.js\";\nimport {\n  assertParts,\n  type PartChildren,\n  type PartComponent,\n  type PartsContract,\n} from \"./internal/parts.js\";\nimport {\n  fadeFromAnchor,\n  PresenceShield,\n  presenceMotion,\n  presenceStyle,\n  useSurfacePresence,\n  useSurfaceTiming,\n} from \"./internal/presence.js\";\nimport {\n  applyMention,\n  mentionMaxVisible,\n  mentionQuery,\n  rankMentions,\n  type MentionQuery,\n} from \"./mention/logic.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport {\n  controlHeight,\n  disabledFade,\n  keepContent,\n  keyboardRing,\n  nativeTextTheme,\n  selectionWash,\n  useTheme,\n} from \"./internal/theme.js\";\n\ninterface MentionContextValue {\n  readonly text: string;\n  readonly query: Option.Option<MentionQuery>;\n  readonly highlighted: number;\n  readonly maxVisible: number;\n\n  readonly disabled: boolean;\n  readonly select: (value: string) => void;\n  readonly close: () => void;\n  readonly reportVisible: (values: ReadonlyArray<string>) => void;\n}\n\nconst MentionContext = createContext<MentionContextValue | undefined>(\n  undefined,\n);\n\nconst MentionItemContext = createContext<{\n  readonly highlighted: boolean;\n  readonly leaving: boolean;\n}>({ highlighted: false, leaving: false });\n\nconst useMention = (): MentionContextValue => {\n  const value = useContext(MentionContext);\n  if (value === undefined) {\n    throw new Error(\"MentionInput parts must be used inside MentionInput\");\n  }\n  return value;\n};\n\nexport interface MentionInputProps extends Omit<\n  InputProps,\n  \"value\" | \"style\" | \"onChange\" | \"onSubmit\"\n> {\n  readonly value?: string;\n  readonly defaultValue?: string;\n  readonly onValueChange?: (value: string) => void;\n\n  readonly onMention?: (value: string) => void;\n\n  readonly label: string;\n\n  readonly maxVisible?: number;\n  readonly disabled?: boolean;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nconst sameValues = (\n  left: ReadonlyArray<string>,\n  right: ReadonlyArray<string>,\n): boolean =>\n  left.length === right.length &&\n  left.every((value, index) => value === right[index]);\n\nexport const MentionInput = ({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  onMention,\n  label,\n  maxVisible = mentionMaxVisible,\n  disabled = false,\n  className,\n  children,\n  placeholder,\n  testId,\n  ...rest\n}: MentionInputProps) => {\n  const theme = useTheme();\n  const [text, setText] = useControllableState({\n    value: valueProp,\n    defaultValue,\n    onChange: onValueChange,\n  });\n  const [dismissed, setDismissed] = useState(false);\n  const [highlighted, setHighlighted] = useState(0);\n  const [visible, setVisible] = useState<ReadonlyArray<string>>([]);\n  assertParts(mentionRoot, children);\n\n  const open = disabled || dismissed ? Option.none() : mentionQuery(text);\n  const active =\n    visible.length === 0\n      ? 0\n      : ((highlighted % visible.length) + visible.length) % visible.length;\n\n  const openKey = Option.isSome(open) ? \"open\" : \"closed\";\n\n  useLayoutEffect(() => {\n    setHighlighted(0);\n  }, [openKey]);\n\n  const select = (value: string): void => {\n    Option.match(open, {\n      onNone: () => undefined,\n      onSome: (query) => {\n        setText(applyMention(text, query.start, value));\n        onMention?.(value);\n        setDismissed(false);\n      },\n    });\n  };\n\n  const context = useMemo<MentionContextValue>(\n    () => ({\n      text,\n      query: open,\n      highlighted: active,\n      maxVisible,\n      disabled,\n      select,\n      close: () => setDismissed(true),\n      reportVisible: (values) =>\n        setVisible((current) =>\n          sameValues(current, values) ? current : values,\n        ),\n    }),\n\n    [active, disabled, maxVisible, open, text],\n  );\n\n  const controlStyle = mergeStyles(recipeStyle(MENTION_INPUT_CONTROL, theme), {\n    ...(disabled ? {} : keyboardRing(theme)),\n    ...selectionWash(theme),\n    ...keepContent,\n  });\n  const hint = fieldHint({ placeholder, disabled }, theme, text, {\n    origin: \"host\",\n    align: \"center\",\n    style: controlStyle,\n  });\n\n  return (\n    <MentionContext.Provider value={context}>\n      <div style={floatingRootStyle()}>\n        <div\n          role=\"group\"\n          aria-label={label}\n          style={mergeStyles(\n            recipeStyle(cn(MENTION_INPUT_FIELD, className), theme),\n            {\n              height: controlHeight(theme, \"md\"),\n              ...disabledFade(disabled),\n            },\n          )}\n        >\n          <div\n            style={hintHost({\n              flexGrow: 1,\n              alignSelf: \"stretch\",\n              minWidth: 0,\n            })}\n          >\n            <input\n              {...rest}\n              theme={nativeTextTheme(theme, \"background\")}\n              aria-label={label}\n              aria-description={fieldHelp(\n                { description: rest[\"aria-description\"], placeholder },\n                text,\n              )}\n              testId={testId}\n              value={text}\n              readOnly={disabled}\n              tabIndex={disabled ? -1 : 0}\n              onChange={(event) => {\n                if (disabled) return;\n                setText(event.value ?? \"\");\n                setDismissed(false);\n              }}\n              onSubmit={() => {\n                if (disabled || visible.length === 0) return;\n                select(visible[active] ?? \"\");\n              }}\n              onKeyDown={(event: EventPayload) => {\n                if (disabled || Option.isNone(open) || visible.length === 0)\n                  return;\n                if (isDown(event)) {\n                  setHighlighted((current) => current + 1);\n                } else if (isUp(event)) {\n                  setHighlighted((current) => current - 1);\n                } else if (isDismiss(event)) {\n                  setDismissed(true);\n                }\n              }}\n              style={controlStyle}\n            />\n            {Option.match(hint, {\n              onNone: () => null,\n              onSome: (painted) => renderFieldHint(painted, testId),\n            })}\n          </div>\n        </div>\n        {children}\n      </div>\n    </MentionContext.Provider>\n  );\n};\n\nexport interface MentionInputListProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\ntype ItemElement = ReactElement<{ readonly value: string }>;\n\nconst isItemElement = (child: unknown): child is ItemElement =>\n  isValidElement<{ readonly value: string }>(child) &&\n  child.type === MentionInputItem &&\n  typeof (child.props as { readonly value?: unknown }).value === \"string\";\n\ninterface PaintedRow {\n  readonly value: string;\n  readonly item: ItemElement;\n}\n\ninterface PaintedList {\n  readonly rows: ReadonlyArray<PaintedRow>;\n  readonly highlighted: number;\n}\n\nconst listFrames = fadeFromAnchor(\"bottom\", 4);\n\nexport const MentionInputList = ({\n  className,\n  children,\n  style,\n  ...rest\n}: MentionInputListProps) => {\n  const theme = useTheme();\n  const { disabled, query, highlighted, maxVisible, reportVisible } =\n    useMention();\n  const timing = useSurfaceTiming(\"normal\");\n  assertParts(mentionList, children);\n\n  const items = Children.toArray(children).filter(isItemElement);\n  const ranked = Option.match(query, {\n    onNone: () => [],\n    onSome: (open) =>\n      rankMentions(\n        items.map((item) => item.props.value),\n        open.query,\n        maxVisible,\n      ),\n  });\n  const rows = Arr.flatMap(ranked, (value) =>\n    Option.match(\n      Arr.findFirst(items, (item) => item.props.value === value),\n      {\n        onNone: () => [],\n        onSome: (item): ReadonlyArray<PaintedRow> => [{ value, item }],\n      },\n    ),\n  );\n\n  const key = ranked.join(\"\\u0000\");\n  useLayoutEffect(() => {\n    reportVisible(ranked);\n  }, [key, reportVisible]);\n\n  const open = Option.isSome(query);\n  const presence = useSurfacePresence(open, timing);\n\n  const shown = useRef<Option.Option<PaintedList>>(Option.none());\n  useLayoutEffect(() => {\n    if (rows.length > 0) shown.current = Option.some({ rows, highlighted });\n  });\n  const leaving = !disabled && !presence.present && presence.mounted;\n  const painted =\n    rows.length > 0\n      ? rows\n      : leaving\n        ? Option.getOrElse(\n            Option.map(shown.current, (last) => last.rows),\n            () => [],\n          )\n        : [];\n  const active =\n    rows.length > 0\n      ? highlighted\n      : Option.getOrElse(\n          Option.map(shown.current, (last) => last.highlighted),\n          () => 0,\n        );\n\n  if (!presence.mounted || painted.length === 0) return null;\n\n  return (\n    <FloatingLayer\n      side=\"bottom\"\n      align=\"start\"\n      sideOffset={4}\n      collisionPadding={8}\n      motion={presenceMotion(listFrames, presence)}\n      role=\"group\"\n      aria-label=\"Mentions\"\n      {...rest}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(MENTION_INPUT_LIST, className), theme), {\n          position: \"relative\",\n          ...presenceStyle(presence.present),\n        }),\n        style,\n      )}\n    >\n      {painted.map((row, index) => (\n        <MentionItemContext.Provider\n          key={row.value}\n          value={{ highlighted: index === active, leaving: !presence.present }}\n        >\n          {row.item}\n        </MentionItemContext.Provider>\n      ))}\n      <PresenceShield present={presence.present} />\n    </FloatingLayer>\n  );\n};\n\nexport interface MentionInputItemProps extends Omit<Props, \"children\"> {\n  readonly value: string;\n\n  readonly label?: string;\n\n  readonly className?: string;\n}\n\nexport const MentionInputItem = ({\n  value,\n  label,\n  className,\n  style,\n  ...rest\n}: MentionInputItemProps) => {\n  const theme = useTheme();\n  const { highlighted, leaving } = useContext(MentionItemContext);\n  const { select } = useMention();\n\n  return (\n    <div\n      role=\"button\"\n      aria-selected={highlighted}\n      tabIndex={leaving ? -1 : 0}\n      {...rest}\n      onMouseDown={() => select(value)}\n      {...activationHandlers(() => select(value))}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(MENTION_INPUT_ITEM, className), theme), {\n          backgroundColor: highlighted ? theme.colors.accent : undefined,\n          color: highlighted\n            ? theme.colors.accentForeground\n            : theme.colors.popoverForeground,\n        }),\n        style,\n      )}\n    >\n      <text style={recipeStyle(MENTION_INPUT_ITEM_TEXT, theme)}>\n        {label ?? `@${value}`}\n      </text>\n    </div>\n  );\n};\n\nconst mentionParts: ReadonlyArray<PartComponent> = [\n  MentionInput,\n  MentionInputList,\n  MentionInputItem,\n];\n\nconst mentionRoot: PartsContract = {\n  parent: \"MentionInput\",\n  family: mentionParts,\n  accepts: [MentionInputList],\n  hint: \"Put MentionInputList directly inside MentionInput.\",\n};\n\nconst mentionList: PartsContract = {\n  parent: \"MentionInputList\",\n  family: mentionParts,\n  accepts: [MentionInputItem],\n  hint: \"Put MentionInputItem rows directly inside MentionInputList.\",\n};\n\nexport const mentionInputMeta = {\n  name: \"Mention input\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the query comes from the text, not the caret: mid-string editing is not supported (0.11.0 has no caret API)\",\n    \"a mention cannot be painted as a link *inside* the field: an `<input>` paints one colour for its whole value, so the field shows plain `@name` and the colour and the link belong to the rendered comment (`mentionRuns` in the docs drawing shows the painted-mirror technique a browser can use)\",\n    \"a space typed while the floating list is mounted is consumed by the runtime before the field sees it; Escape or a selection closes the list\",\n    \"Tab insertion is not claimed: 0.11.0 moves focus at the platform level\",\n    \"Enter arrives as the input's submit event, so it is handled there\",\n    \"role=listbox and role=option are dropped from 0.11.0's accessibility dump, so the surface is a named group and rows are selected buttons\",\n    \"no aria-activedescendant in 0.11.0\",\n    \"maxVisible is a row budget: an unreadable one (NaN from a height measured before layout) reads as the house 6, not as no rows\",\n  ],\n  docs: \"docs/mention-input.mdx\",\n  example: \"examples/mention-input.example.tsx\",\n  testFile: \"src/behaviour/mention-input.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/mention/logic.ts",
      "target": "@ui/gpuixui/components/mention/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\n\nexport interface MentionQuery {\n  readonly start: number;\n  readonly query: string;\n}\n\nexport const mentionQuery = (text: string): Option.Option<MentionQuery> => {\n  const at = Str.lastIndexOf(\"@\")(text);\n  if (Option.isNone(at)) return Option.none();\n  const start = at.value;\n  const before = start === 0 ? \"\" : Str.slice(start - 1, start)(text);\n  const query = Str.slice(start + 1)(text);\n  if (before !== \"\" && before !== \" \" && before !== \"\\n\") return Option.none();\n  if (Str.includes(\" \")(query) || Str.includes(\"\\n\")(query)) {\n    return Option.none();\n  }\n  return Option.some({ start, query });\n};\n\nexport const applyMention = (\n  text: string,\n  start: number,\n  value: string,\n): string => `${Str.slice(0, start)(text)}@${value} `;\n\nexport const mentionMaxVisible = 6;\n\nexport const normalizeMaxVisible = (maxVisible: number): number =>\n  Number.isNaN(maxVisible)\n    ? mentionMaxVisible\n    : Math.max(0, Math.floor(maxVisible));\n\nexport const rankMentions = (\n  values: ReadonlyArray<string>,\n  query: string,\n  maxVisible: number,\n): ReadonlyArray<string> => {\n  const needle = Str.toLowerCase(query);\n  const matches = Arr.filter(values, (value) =>\n    Str.includes(needle)(Str.toLowerCase(value)),\n  );\n  const prefixes = Arr.filter(matches, (value) =>\n    Str.startsWith(needle)(Str.toLowerCase(value)),\n  );\n  const rest = Arr.filter(\n    matches,\n    (value) => !Str.startsWith(needle)(Str.toLowerCase(value)),\n  );\n  return Arr.take(\n    Arr.appendAll(prefixes, rest),\n    normalizeMaxVisible(maxVisible),\n  );\n};\n"
    }
  ],
  "docs": "[Mention input](https://gpuixui.sudokw.com/components/mention-input) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/mention-input",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}