{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "emoji-picker",
  "title": "Emoji Picker",
  "author": "@fma-ds/ds",
  "description": "An emoji picker with search, skin tones, recently used emoji, custom emoji and a virtualized grid.\n\n`<EmojiPicker />` alone renders a trigger and a popover. Compose `EmojiPickerTrigger` + `EmojiPickerContent`, or `EmojiPickerPanel` in place, from `EmojiPickerSearch`, `EmojiPickerCategories`, `EmojiPickerList`, `EmojiPickerPreview` and `EmojiPickerSkinTone`.\n\n- **Search**: names, keywords and shortcodes.\n- **Categories**: a category bar that follows the scroll, plus a frequently or recently used section.\n- **Data**: emojibase, fetched once per page from jsDelivr (`dataUrl` to self-host, `locale` for 28 languages) or passed as `data`. `emojiVersion=\"auto\"` hides emoji the device can't draw.\n- **Value**: `value`/`defaultValue`/`onChange` hold the native emoji (or `:shortcode:` for a custom one). `selectionMode=\"multiple\"` toggles a list (with `max`), and `onEmojiSelect` fires on every pick, for inserting into text.\n- **Keyboard**: focus stays in the search while the arrows move through the grid (a combobox over an `aria-activedescendant` listbox); Enter picks.\n- **Persistence**: recent picks and the tone persist in localStorage (`recentStorageKey`, `skinToneStorageKey`; `null` keeps them in memory).\n- **Forms**: `name` submits with a native form.\n\nEvery string comes from `i18n`.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/dropdown-menu",
    "@fma-ds/empty",
    "@fma-ds/input-group",
    "@fma-ds/popover",
    "@fma-ds/skeleton",
    "@fma-ds/tabs"
  ],
  "files": [
    {
      "path": "src/core/emoji-picker/emoji-picker.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport {\n  CarIcon,\n  ClockIcon,\n  CoffeeIcon,\n  FlagIcon,\n  HeartIcon,\n  LeafIcon,\n  LightbulbIcon,\n  SearchIcon,\n  SmileIcon,\n  SmilePlusIcon,\n  SparklesIcon,\n  UserRoundIcon,\n  VolleyballIcon,\n  XIcon,\n} from \"lucide-react\";\nimport type {\n  ComponentProps,\n  CSSProperties,\n  KeyboardEvent as ReactKeyboardEvent,\n  ReactNode,\n  Ref,\n  RefObject,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { Dialog } from \"react-aria-components\";\n\nimport { Button } from \"@/core/button/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@/core/dropdown-menu/dropdown-menu\";\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/core/empty/empty\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/core/input-group/input-group\";\nimport { Popover } from \"@/core/popover/popover\";\nimport { Skeleton } from \"@/core/skeleton/skeleton\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/core/tabs/tabs\";\n\n/* -------------------------------------------------------------------------- */\n/*                                    Types                                   */\n/* -------------------------------------------------------------------------- */\n\n/** 0 is the default yellow; 1 to 5 run from light to dark. */\ntype EmojiPickerSkinTone = 0 | 1 | 2 | 3 | 4 | 5;\n\n/** The Unicode categories, in their standard order. */\ntype EmojiPickerCategory =\n  | \"smileys\"\n  | \"people\"\n  | \"nature\"\n  | \"food\"\n  | \"travel\"\n  | \"activities\"\n  | \"objects\"\n  | \"symbols\"\n  | \"flags\";\n\ntype EmojiPickerEmoji = {\n  /** The hexcode of a Unicode emoji, like `1F44D`; the given id of a custom one. */\n  id: string;\n  /** The native emoji at the default tone. Empty for a custom emoji. */\n  emoji: string;\n  /** Lowercase name in the data's language, like `thumbs up`. */\n  label: string;\n  /** Search keywords in the data's language. */\n  tags: string[];\n  /** Without colons, like `+1` and `thumbsup`. */\n  shortcodes: string[];\n  /** A category key, or a custom category id. */\n  category: string;\n  /** The Emoji version that added it. 0 for a custom emoji. */\n  version: number;\n  /** The five toned forms, light to dark, when the emoji takes a skin tone. */\n  skins?: string[];\n  /** The Emoji version that added the tones, when later than the emoji's own. */\n  skinVersion?: number;\n  /** Image URL of a custom emoji. */\n  src?: string;\n};\n\ntype EmojiPickerCustomEmoji = {\n  id: string;\n  label: string;\n  /** Image URL. A square image of 64px or more stays sharp at every size. */\n  src: string;\n  tags?: string[];\n  /** Defaults to the id. The first one makes the value: `:partyparrot:`. */\n  shortcodes?: string[];\n  /** A `customCategories` id. Defaults to `custom`. */\n  category?: string;\n};\n\ntype EmojiPickerCustomCategory = {\n  id: string;\n  label: string;\n  /** Tab icon. Defaults to a sparkle. */\n  icon?: ReactNode;\n};\n\ntype EmojiPickerData = {\n  locale: string;\n  emojis: EmojiPickerEmoji[];\n  /** Category names in the data's language. */\n  categories: Partial<Record<EmojiPickerCategory, string>>;\n};\n\n/** What a pick hands back. */\ntype EmojiPickerSelection = {\n  /** The native emoji with the skin tone applied, or `:shortcode:` for a custom emoji. */\n  value: string;\n  emoji: EmojiPickerEmoji;\n  skinTone: EmojiPickerSkinTone;\n};\n\n/** One frequently used emoji, by base id. `count` orders the frequent mode. */\ntype EmojiPickerRecentEntry = { id: string; count: number };\n\n/** GitHub and Slack (`iamcal`) codes are English only; `emojibase` and `cldr` follow the locale. */\ntype EmojiPickerShortcodePreset = \"github\" | \"iamcal\" | \"emojibase\" | \"cldr\";\n\ntype EmojiPickerSize = \"sm\" | \"default\" | \"lg\";\n\ntype EmojiPickerDataStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\ntype EmojiPickerLabels = {\n  /** The trigger's name while there is no value. */\n  trigger: string;\n  /** Names the popup and an inline panel. */\n  panel: string;\n  /** Search placeholder and name. */\n  search: string;\n  clearSearch: string;\n  /** Names the grid. */\n  list: string;\n  /** Names the category tabs. */\n  categories: string;\n  /** The recent section under `recentMode=\"frequent\"`. */\n  frequent: string;\n  /** The recent section under `recentMode=\"recent\"`. */\n  recent: string;\n  searchResults: string;\n  /** Custom emoji with no `customCategories` entry. */\n  custom: string;\n  smileys: string;\n  people: string;\n  nature: string;\n  food: string;\n  travel: string;\n  activities: string;\n  objects: string;\n  symbols: string;\n  flags: string;\n  skinTone: string;\n  /** Default, then light to dark. */\n  skinTones: [string, string, string, string, string, string];\n  empty: string;\n  emptyHint: string;\n  loading: string;\n  error: string;\n  retry: string;\n  /** The preview while nothing is highlighted. */\n  preview: string;\n};\n\ntype EmojiPickerFunctions = {\n  /** Announced after a search. */\n  formatResultCount: (count: number) => string;\n  /** Announced after a pick that keeps the picker open. */\n  formatSelected: (label: string) => string;\n  /** The display name, from the lowercase data label. */\n  formatLabel: (label: string) => string;\n};\n\ntype EmojiPickerI18nConfig = {\n  labels: EmojiPickerLabels;\n  functions: EmojiPickerFunctions;\n};\n\n/** One level deeper than `Partial`, so a single label can be overridden. */\ntype EmojiPickerI18nOverrides = {\n  labels?: Partial<EmojiPickerLabels>;\n  functions?: Partial<EmojiPickerFunctions>;\n};\n\nconst DEFAULT_EMOJI_PICKER_I18N: EmojiPickerI18nConfig = {\n  labels: {\n    trigger: \"Add emoji\",\n    panel: \"Emoji picker\",\n    search: \"Search emoji\",\n    clearSearch: \"Clear search\",\n    list: \"Emoji\",\n    categories: \"Emoji categories\",\n    frequent: \"Frequently used\",\n    recent: \"Recently used\",\n    searchResults: \"Search results\",\n    custom: \"Custom\",\n    smileys: \"Smileys & emotion\",\n    people: \"People & body\",\n    nature: \"Animals & nature\",\n    food: \"Food & drink\",\n    travel: \"Travel & places\",\n    activities: \"Activities\",\n    objects: \"Objects\",\n    symbols: \"Symbols\",\n    flags: \"Flags\",\n    skinTone: \"Skin tone\",\n    skinTones: [\"Default\", \"Light\", \"Medium-light\", \"Medium\", \"Medium-dark\", \"Dark\"],\n    empty: \"No emoji found\",\n    emptyHint: \"Try another word or a shortcode\",\n    loading: \"Loading emoji\",\n    error: \"Couldn't load emoji\",\n    retry: \"Try again\",\n    preview: \"Pick an emoji\",\n  },\n  functions: {\n    formatResultCount: (count) => (count === 1 ? \"1 emoji found\" : `${count} emoji found`),\n    formatSelected: (label) => `${label} selected`,\n    formatLabel: (label) => label.charAt(0).toLocaleUpperCase() + label.slice(1),\n  },\n};\n\n/** Merges per section, so an override can carry one key without restating the rest. */\nfunction mergeEmojiPickerI18n(overrides?: EmojiPickerI18nOverrides): EmojiPickerI18nConfig {\n  if (!overrides?.labels && !overrides?.functions) {\n    return DEFAULT_EMOJI_PICKER_I18N;\n  }\n  return {\n    labels: { ...DEFAULT_EMOJI_PICKER_I18N.labels, ...overrides.labels },\n    functions: {\n      ...DEFAULT_EMOJI_PICKER_I18N.functions,\n      ...overrides.functions,\n    },\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Data                                    */\n/* -------------------------------------------------------------------------- */\n\n/** The pinned emojibase-data release fetched by default. An exact version caches for good. */\nconst EMOJI_PICKER_DATA_URL = \"https://cdn.jsdelivr.net/npm/emojibase-data@17.0.0\";\n\nconst EMOJI_PICKER_CATEGORIES: EmojiPickerCategory[] = [\n  \"smileys\",\n  \"people\",\n  \"nature\",\n  \"food\",\n  \"travel\",\n  \"activities\",\n  \"objects\",\n  \"symbols\",\n  \"flags\",\n];\n\n/* emojibase group numbers. Group 2 holds the bare tone and hair components. */\nconst GROUPS: Array<EmojiPickerCategory | null> = [\n  \"smileys\",\n  \"people\",\n  null,\n  \"nature\",\n  \"food\",\n  \"travel\",\n  \"activities\",\n  \"objects\",\n  \"symbols\",\n  \"flags\",\n];\n\nconst MESSAGE_GROUPS: Record<string, EmojiPickerCategory> = {\n  \"smileys-emotion\": \"smileys\",\n  \"people-body\": \"people\",\n  \"animals-nature\": \"nature\",\n  \"food-drink\": \"food\",\n  \"travel-places\": \"travel\",\n  activities: \"activities\",\n  objects: \"objects\",\n  symbols: \"symbols\",\n  flags: \"flags\",\n};\n\nconst ENGLISH_ONLY_SHORTCODES = new Set([\"github\", \"iamcal\"]);\n\n/* The locale lands in a URL path, so only a BCP 47 shape gets through. */\nconst LOCALE_PATTERN = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/i;\n\ntype EmojiPickerDataOptions = {\n  /** An emojibase locale: `en`, `de`, `ja`, `pt`, `zh-hant` and 23 more. */\n  locale?: string;\n  /** Base URL of an emojibase-data copy, for self-hosting or a stricter CSP. */\n  url?: string;\n  shortcodes?: EmojiPickerShortcodePreset | EmojiPickerShortcodePreset[] | false;\n};\n\ntype RawSkin = { emoji?: unknown; tone?: unknown; version?: unknown };\ntype RawEmoji = {\n  label?: unknown;\n  hexcode?: unknown;\n  emoji?: unknown;\n  tags?: unknown;\n  group?: unknown;\n  order?: unknown;\n  version?: unknown;\n  skins?: unknown;\n};\n\nconst asNumber = (value: unknown) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n\nconst asStrings = (value: unknown): string[] =>\n  Array.isArray(value)\n    ? value.filter((item): item is string => typeof item === \"string\")\n    : typeof value === \"string\"\n      ? [value]\n      : [];\n\n/**\n * Turns emojibase's `data.json`, `messages.json` and shortcode files into\n * picker data. Entries it cannot read are dropped rather than trusted.\n */\nfunction normalizeEmojibaseData(\n  raw: unknown,\n  options: { locale?: string; messages?: unknown; shortcodes?: unknown[] } = {},\n): EmojiPickerData {\n  if (!Array.isArray(raw)) throw new Error(\"Unexpected emoji data\");\n  const locale = options.locale ?? \"en\";\n  const codes = new Map<string, string[]>();\n  for (const map of options.shortcodes ?? []) {\n    if (!map || typeof map !== \"object\") continue;\n    for (const [hexcode, value] of Object.entries(map)) {\n      const known = codes.get(hexcode) ?? [];\n      const added = asStrings(value).filter((code) => !known.includes(code));\n      codes.set(hexcode, [...known, ...added]);\n    }\n  }\n  const sorted: Array<EmojiPickerEmoji & { order: number }> = [];\n  for (const item of raw as RawEmoji[]) {\n    const category = typeof item?.group === \"number\" ? GROUPS[item.group] : undefined;\n    if (!category) continue;\n    if (typeof item.emoji !== \"string\" || typeof item.hexcode !== \"string\") {\n      continue;\n    }\n    /* Mixed tones in one sequence (two people, two tones) are left out: a\n       single tone choice cannot reach them. */\n    const skins: string[] = [];\n    let skinVersion = 0;\n    for (const skin of (Array.isArray(item.skins) ? item.skins : []) as RawSkin[]) {\n      const tone = skin?.tone;\n      if (typeof tone !== \"number\" || tone < 1 || tone > 5) continue;\n      if (typeof skin.emoji !== \"string\") continue;\n      skins[tone - 1] = skin.emoji;\n      skinVersion = Math.max(skinVersion, asNumber(skin.version));\n    }\n    const version = asNumber(item.version);\n    sorted.push({\n      id: item.hexcode,\n      emoji: item.emoji,\n      label: typeof item.label === \"string\" ? item.label : \"\",\n      tags: asStrings(item.tags),\n      shortcodes: codes.get(item.hexcode) ?? [],\n      category,\n      version,\n      order: asNumber(item.order),\n      ...(skins.filter(Boolean).length === 5 && {\n        skins,\n        ...(skinVersion > version && { skinVersion }),\n      }),\n    });\n  }\n  sorted.sort((a, b) => a.order - b.order);\n  const emojis = sorted.map(({ order: _order, ...emoji }) => emoji);\n\n  const categories: EmojiPickerData[\"categories\"] = {};\n  const groups = (options.messages as { groups?: unknown } | undefined)?.groups;\n  for (const group of Array.isArray(groups) ? groups : []) {\n    const key = MESSAGE_GROUPS[group?.key];\n    if (key && typeof group.message === \"string\" && group.message) {\n      categories[key] = group.message.charAt(0).toLocaleUpperCase(locale) + group.message.slice(1);\n    }\n  }\n  return { locale, emojis, categories };\n}\n\nconst dataCache = new Map<string, Promise<EmojiPickerData>>();\n/* Settled loads, read while rendering so a second picker never flashes a\n   loading state for data the page already has. */\nconst dataReady = new Map<string, EmojiPickerData>();\n\nfunction presetsOf(shortcodes: EmojiPickerDataOptions[\"shortcodes\"]) {\n  return shortcodes === false ? [] : [shortcodes ?? \"github\"].flat();\n}\n\nfunction dataKey(options: EmojiPickerDataOptions) {\n  const presets = presetsOf(options.shortcodes);\n  return [options.url ?? EMOJI_PICKER_DATA_URL, options.locale ?? \"en\", presets.join(\"+\")].join(\n    \"|\",\n  );\n}\n\nasync function fetchEmojiPickerData(options: EmojiPickerDataOptions): Promise<EmojiPickerData> {\n  const locale = options.locale ?? \"en\";\n  if (!LOCALE_PATTERN.test(locale)) {\n    throw new Error(`Invalid emoji locale: ${locale}`);\n  }\n  const base = (options.url ?? EMOJI_PICKER_DATA_URL).replace(/\\/+$/, \"\");\n  const json = async (path: string) => {\n    const response = await fetch(`${base}/${path}`);\n    if (!response.ok) throw new Error(`${response.status} for ${path}`);\n    return response.json() as Promise<unknown>;\n  };\n  /* Only the emoji themselves are required; names and codes degrade. */\n  const optional = (path: string) => json(path).catch(() => undefined);\n  const [raw, messages, ...shortcodes] = await Promise.all([\n    json(`${locale}/data.json`),\n    optional(`${locale}/messages.json`),\n    ...presetsOf(options.shortcodes).map((preset) =>\n      optional(`${ENGLISH_ONLY_SHORTCODES.has(preset) ? \"en\" : locale}/shortcodes/${preset}.json`),\n    ),\n  ]);\n  return normalizeEmojibaseData(raw, { locale, messages, shortcodes });\n}\n\n/**\n * Fetches and normalizes emojibase data once per URL, locale and shortcode\n * set; every picker on the page shares the request. A failed load is\n * forgotten, so the next call retries.\n */\nfunction loadEmojiPickerData(options: EmojiPickerDataOptions = {}): Promise<EmojiPickerData> {\n  const key = dataKey(options);\n  let pending = dataCache.get(key);\n  if (!pending) {\n    pending = fetchEmojiPickerData(options).then(\n      (data) => {\n        dataReady.set(key, data);\n        return data;\n      },\n      (error: unknown) => {\n        dataCache.delete(key);\n        throw error;\n      },\n    );\n    dataCache.set(key, pending);\n  }\n  return pending;\n}\n\ntype UseEmojiPickerDataOptions = EmojiPickerDataOptions & {\n  /** Data already in hand, bundled or loaded on the server. Skips the fetch. */\n  data?: EmojiPickerData;\n  /** Holds the fetch until true. */\n  enabled?: boolean;\n};\n\ntype EmojiPickerDataState = {\n  data: EmojiPickerData | null;\n  status: EmojiPickerDataStatus;\n  error: unknown;\n  retry: () => void;\n};\n\n/** Loads emoji data for a custom surface: a shortcode autocomplete, a reaction bar. */\nfunction useEmojiPickerData(options: UseEmojiPickerDataOptions = {}): EmojiPickerDataState {\n  const { data: provided, enabled = true, locale = \"en\", url } = options;\n  /* A string, so an inline array does not refetch every render. */\n  const presetKey = presetsOf(options.shortcodes).join(\"+\");\n  const key = dataKey({ locale, url, shortcodes: options.shortcodes });\n  const [result, setResult] = useState<{\n    key: string;\n    data?: EmojiPickerData;\n    error?: unknown;\n  } | null>(null);\n  const [attempt, setAttempt] = useState(0);\n  const ready = dataReady.get(key);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `attempt` re-runs the load on retry\n  useEffect(() => {\n    if (provided || !enabled || dataReady.has(key)) return;\n    let alive = true;\n    const shortcodes = presetKey ? (presetKey.split(\"+\") as EmojiPickerShortcodePreset[]) : false;\n    loadEmojiPickerData({ locale, url, shortcodes }).then(\n      (data) => alive && setResult({ key, data }),\n      (error: unknown) => alive && setResult({ key, error }),\n    );\n    return () => {\n      alive = false;\n    };\n  }, [provided, enabled, key, locale, url, presetKey, attempt]);\n\n  const current = result?.key === key ? result : null;\n  const data = provided ?? current?.data ?? ready ?? null;\n  return {\n    data,\n    status: data ? \"ready\" : current?.error !== undefined ? \"error\" : enabled ? \"loading\" : \"idle\",\n    error: current?.error,\n    retry: () => {\n      setResult(null);\n      setAttempt((count) => count + 1);\n    },\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Support                                  */\n/* -------------------------------------------------------------------------- */\n\n/* Apple first, then Firefox's bundled Twemoji (it has flags on Windows, where\n   Segoe UI Emoji does not), then Windows and Android. */\nconst EMOJI_FONT =\n  '\"Apple Color Emoji\", \"Twemoji Mozilla\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", sans-serif';\n\ntype EmojiSupport = { version: number; flags: boolean };\n\nconst FULL_SUPPORT: EmojiSupport = { version: Infinity, flags: true };\nconst supportCache = new WeakMap<EmojiPickerEmoji[], EmojiSupport>();\nconst PROBE = 20;\n\n/* A color glyph ignores fillStyle, so red and blue draws match pixel for\n   pixel; a missing glyph's box takes the color. A ZWJ sequence the font\n   cannot join draws as several glyphs, wider than one. */\nfunction drawsAsEmoji(context: CanvasRenderingContext2D, text: string) {\n  const draw = (color: string) => {\n    context.clearRect(0, 0, PROBE * 2, PROBE * 2);\n    context.fillStyle = color;\n    context.fillText(text, 0, 0);\n    return context.getImageData(0, 0, PROBE * 2, PROBE).data;\n  };\n  const red = draw(\"#f00\");\n  const blue = draw(\"#00f\");\n  let ink = false;\n  for (let index = 3; index < red.length; index += 4) {\n    if (!red[index] && !blue[index]) continue;\n    ink = true;\n    if (red[index - 3] !== blue[index - 3] || red[index - 1] !== blue[index - 1]) {\n      return false;\n    }\n  }\n  return ink && context.measureText(text).width < PROBE * 1.6;\n}\n\n/**\n * The newest Emoji version this device draws, probed with the first emoji of\n * each version in the data, newest first, plus a flag. A probe that fails\n * even the oldest version is unreliable (blocked or noised canvas), so it\n * reports full support rather than an empty grid.\n */\nfunction detectSupport(emojis: EmojiPickerEmoji[]): EmojiSupport {\n  const cached = supportCache.get(emojis);\n  if (cached) return cached;\n  let support = FULL_SUPPORT;\n  const canvas = typeof document === \"undefined\" ? null : document.createElement(\"canvas\");\n  canvas?.setAttribute(\"width\", String(PROBE * 2));\n  canvas?.setAttribute(\"height\", String(PROBE * 2));\n  const context = canvas?.getContext(\"2d\", { willReadFrequently: true });\n  if (context) {\n    context.font = `${PROBE}px ${EMOJI_FONT}`;\n    context.textBaseline = \"top\";\n    const samples = new Map<number, string>();\n    for (const emoji of emojis) {\n      if (emoji.src || isRegionalFlag(emoji.emoji)) continue;\n      if (!samples.has(emoji.version)) samples.set(emoji.version, emoji.emoji);\n    }\n    const versions = [...samples.keys()].sort((a, b) => b - a);\n    const found = versions.find((version) => drawsAsEmoji(context, samples.get(version) as string));\n    if (found !== undefined) {\n      support = {\n        version: found === versions[0] ? Infinity : found,\n        flags: drawsAsEmoji(context, \"\\u{1F1EA}\\u{1F1FA}\"),\n      };\n    }\n  }\n  supportCache.set(emojis, support);\n  return support;\n}\n\n/* Country flags (two regional indicators) and subdivision flags (black flag\n   plus tag characters): what Windows draws as letters. */\nfunction isRegionalFlag(text: string) {\n  return /^(?:[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0060}-\\u{E007F}]+)$/u.test(text);\n}\n\nconst subscribeNothing = () => () => {};\n\n/* Read through an external store: the server and the hydrating client both\n   see full support, then the client re-renders once with what it can draw. */\nfunction useEmojiSupport(data: EmojiPickerData | null, setting: number | \"auto\"): EmojiSupport {\n  const detected = useSyncExternalStore(\n    subscribeNothing,\n    () => (setting === \"auto\" && data ? detectSupport(data.emojis) : FULL_SUPPORT),\n    () => FULL_SUPPORT,\n  );\n  /* One identity per setting: a fresh object each render would re-filter the\n     whole data set on every hover. */\n  return useMemo(\n    () => (setting === \"auto\" ? detected : { version: setting, flags: true }),\n    [setting, detected],\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Search                                   */\n/* -------------------------------------------------------------------------- */\n\n/* Accents drop, and hiragana reads as katakana, so ねこ finds ネコ. */\nconst fold = (text: string) =>\n  text\n    .normalize(\"NFKD\")\n    .replace(/\\p{M}/gu, \"\")\n    .toLowerCase()\n    .replace(/[\\u3041-\\u3096]/g, (kana) => String.fromCharCode(kana.charCodeAt(0) + 0x60));\n\ntype SearchIndex = {\n  label: string;\n  words: string[];\n  tags: string[];\n  shortcodes: string[];\n};\n\nconst searchIndexCache = new WeakMap<EmojiPickerEmoji, SearchIndex>();\n\nfunction searchIndexOf(emoji: EmojiPickerEmoji) {\n  let index = searchIndexCache.get(emoji);\n  if (!index) {\n    const label = fold(emoji.label);\n    index = {\n      label,\n      words: label.split(/[\\s:,-]+/).filter(Boolean),\n      tags: emoji.tags.map(fold),\n      shortcodes: emoji.shortcodes.map(fold),\n    };\n    searchIndexCache.set(emoji, index);\n  }\n  return index;\n}\n\n/* Best field match for one term: exact beats a prefix beats a substring, and\n   a name or shortcode beats a keyword. */\nfunction scoreTerm(index: SearchIndex, term: string) {\n  let best = 0;\n  const take = (score: number) => {\n    if (score > best) best = score;\n  };\n  if (index.label === term) take(100);\n  for (const code of index.shortcodes) {\n    if (code === term) take(100);\n    else if (code.startsWith(term)) take(80);\n    else if (code.includes(term)) take(30);\n  }\n  if (index.label.startsWith(term)) take(75);\n  for (const word of index.words) if (word.startsWith(term)) take(70);\n  for (const tag of index.tags) {\n    if (tag === term) take(60);\n    else if (tag.startsWith(term)) take(50);\n    else if (tag.includes(term)) take(20);\n  }\n  if (best === 0 && index.label.includes(term)) take(35);\n  return best;\n}\n\n/**\n * Ranks emoji against a query. Every word must match the name, a keyword or\n * a shortcode; colons around a shortcode are ignored, so `:thumbsup:` works.\n * Ties keep the input order.\n */\nfunction searchEmojis(\n  emojis: EmojiPickerEmoji[],\n  query: string,\n  options: { limit?: number } = {},\n): EmojiPickerEmoji[] {\n  const terms = fold(query).replace(/:/g, \" \").split(/\\s+/).filter(Boolean);\n  if (terms.length === 0) return [];\n  const exact = query.trim();\n  const scored: Array<{ emoji: EmojiPickerEmoji; score: number; at: number }> = [];\n  emojis.forEach((emoji, at) => {\n    if (exact && isEmojiValue(emoji, exact)) {\n      scored.push({ emoji, score: 1000, at });\n      return;\n    }\n    const index = searchIndexOf(emoji);\n    let score = 0;\n    for (const term of terms) {\n      const termScore = scoreTerm(index, term);\n      if (termScore === 0) return;\n      score += termScore;\n    }\n    scored.push({ emoji, score, at });\n  });\n  scored.sort((a, b) => b.score - a.score || a.at - b.at);\n  const ranked = scored.map((entry) => entry.emoji);\n  return options.limit === undefined ? ranked : ranked.slice(0, options.limit);\n}\n\n/** The value a pick stores: the toned native emoji, or `:shortcode:` for a custom one. */\nfunction getEmojiValue(emoji: EmojiPickerEmoji, skinTone: EmojiPickerSkinTone = 0): string {\n  if (emoji.src) return `:${emoji.shortcodes[0] ?? emoji.id}:`;\n  return (skinTone > 0 && emoji.skins?.[skinTone - 1]) || emoji.emoji;\n}\n\n/* emojibase writes the fully qualified form (thumbs up with VS16), while\n   stored text and older data often carry the bare one, so values compare\n   without the selector. */\nconst bare = (text: string) => text.replace(/\\uFE0F/g, \"\");\n\n/** Whether a stored value is this emoji: any tone, with or without VS16. */\nfunction isEmojiValue(emoji: EmojiPickerEmoji, value: string) {\n  const target = bare(value);\n  return (\n    bare(getEmojiValue(emoji)) === target || !!emoji.skins?.some((skin) => bare(skin) === target)\n  );\n}\n\n/** Finds the emoji a value came from, any tone, or a custom emoji's `:shortcode:`. */\nfunction findEmoji(\n  emojis: EmojiPickerEmoji[],\n  value: string | null | undefined,\n): EmojiPickerEmoji | undefined {\n  if (!value) return undefined;\n  return emojis.find((emoji) => isEmojiValue(emoji, value));\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Storage                                  */\n/* -------------------------------------------------------------------------- */\n\nconst storageListeners = new Map<string, Set<() => void>>();\n/* Writes storage refused (private mode, quota, a blocked origin), kept for\n   this page so the picker still remembers within the visit. */\nconst storageMemory = new Map<string, string>();\n\nfunction readStored(key: string): string | null {\n  if (storageMemory.has(key)) return storageMemory.get(key) as string;\n  try {\n    return window.localStorage.getItem(key);\n  } catch {\n    return null;\n  }\n}\n\nfunction writeStored(key: string, value: string) {\n  try {\n    window.localStorage.setItem(key, value);\n    storageMemory.delete(key);\n  } catch {\n    storageMemory.set(key, value);\n  }\n  for (const listener of storageListeners.get(key) ?? []) listener();\n}\n\nfunction subscribeStored(key: string, listener: () => void) {\n  let listeners = storageListeners.get(key);\n  if (!listeners) {\n    listeners = new Set();\n    storageListeners.set(key, listeners);\n  }\n  listeners.add(listener);\n  /* Another tab's write: drop this tab's copy and re-read. */\n  const onStorage = (event: StorageEvent) => {\n    if (event.key !== key && event.key !== null) return;\n    storageMemory.delete(key);\n    listener();\n  };\n  window.addEventListener(\"storage\", onStorage);\n  return () => {\n    listeners.delete(listener);\n    window.removeEventListener(\"storage\", onStorage);\n  };\n}\n\n/* One stored string, shared live by every picker and tab using the key. The\n   server renders nothing stored, so markup never depends on the visitor. */\nfunction useStored(key: string | null) {\n  const subscribe = useCallback(\n    (listener: () => void) => (key ? subscribeStored(key, listener) : () => {}),\n    [key],\n  );\n  return useSyncExternalStore(\n    subscribe,\n    () => (key ? readStored(key) : null),\n    () => null,\n  );\n}\n\nfunction parseRecent(raw: string | null): EmojiPickerRecentEntry[] | null {\n  if (!raw) return null;\n  try {\n    const list: unknown = JSON.parse(raw);\n    if (!Array.isArray(list)) return null;\n    return list.flatMap((entry) =>\n      entry && typeof entry.id === \"string\"\n        ? [{ id: entry.id, count: Math.max(1, asNumber(entry.count)) }]\n        : [],\n    );\n  } catch {\n    return null;\n  }\n}\n\nfunction parseTone(raw: string | null): EmojiPickerSkinTone | null {\n  const tone = Number(raw);\n  return raw !== null && Number.isInteger(tone) && tone >= 0 && tone <= 5\n    ? (tone as EmojiPickerSkinTone)\n    : null;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Layout                                   */\n/* -------------------------------------------------------------------------- */\n\n/* The panel's padding: the search, the tabs, the section names, the first\n   column of glyphs and the preview all start this far in, as the shadcn\n   Command lines up its input, headings and items. */\nconst PADDING = 8;\n\n/* Pixel geometry the virtual grid computes with. Cells and headers are\n   fixed-height on purpose: a measured row would cost a layout pass per row.\n   Glyphs fill a bit over half their cell, so a dense grid still scans. The\n   gutter is what is left of the padding once a cell's own slack is counted,\n   so a hovered cell reaches past the padding the way a menu item does. */\nconst sizeOf = (cell: number, emoji: number, header: number) => ({\n  cell,\n  emoji,\n  header,\n  gutter: PADDING - (cell - emoji) / 2,\n});\n\ntype SizeTokens = ReturnType<typeof sizeOf>;\n\nconst SIZES: Record<EmojiPickerSize, SizeTokens> = {\n  sm: sizeOf(26, 16, 28),\n  default: sizeOf(30, 20, 28),\n  lg: sizeOf(38, 26, 32),\n};\n\nconst VISIBLE_ROWS = 7;\nconst OVERSCAN = 4;\nconst RECENT_CAP = 48;\n/* jsdom and an unmeasured first frame report 0; this keeps the window sane. */\nconst FALLBACK_HEIGHT = 320;\n\ntype Section = {\n  id: string;\n  label: string;\n  emojis: EmojiPickerEmoji[];\n  icon: ReactNode;\n};\n\ntype LayoutSection = {\n  section: Section;\n  top: number;\n  height: number;\n  firstRow: number;\n};\n\ntype LayoutRow = { section: number; top: number; cells: EmojiPickerEmoji[] };\n\ntype Layout = {\n  sections: LayoutSection[];\n  rows: LayoutRow[];\n  height: number;\n  positions: Map<string, { row: number; col: number }>;\n};\n\nconst cellKey = (section: string, id: string) => `${section}\\u0000${id}`;\n\nfunction buildLayout(sections: Section[], columns: number, size: SizeTokens): Layout {\n  const layoutSections: LayoutSection[] = [];\n  const rows: LayoutRow[] = [];\n  const positions = new Map<string, { row: number; col: number }>();\n  let top = 0;\n  sections.forEach((section, index) => {\n    const firstRow = rows.length;\n    const count = Math.ceil(section.emojis.length / columns);\n    for (let row = 0; row < count; row++) {\n      const cells = section.emojis.slice(row * columns, (row + 1) * columns);\n      cells.forEach((emoji, col) => {\n        positions.set(cellKey(section.id, emoji.id), { row: rows.length, col });\n      });\n      rows.push({\n        section: index,\n        top: top + size.header + row * size.cell,\n        cells,\n      });\n    }\n    const height = size.header + count * size.cell;\n    layoutSections.push({ section, top, height, firstRow });\n    top += height;\n  });\n  return {\n    sections: layoutSections,\n    rows,\n    height: top + size.gutter,\n    positions,\n  };\n}\n\n/* The last item whose top is at or above `offset`. */\nfunction indexAt(items: Array<{ top: number }>, offset: number) {\n  let low = 0;\n  let high = items.length - 1;\n  let found = 0;\n  while (low <= high) {\n    const middle = (low + high) >> 1;\n    if (items[middle].top <= offset) {\n      found = middle;\n      low = middle + 1;\n    } else {\n      high = middle - 1;\n    }\n  }\n  return found;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Icons                                   */\n/* -------------------------------------------------------------------------- */\n\nconst CATEGORY_ICONS: Record<string, ReactNode> = {\n  recent: <ClockIcon aria-hidden=\"true\" />,\n  smileys: <SmileIcon aria-hidden=\"true\" />,\n  people: <UserRoundIcon aria-hidden=\"true\" />,\n  nature: <LeafIcon aria-hidden=\"true\" />,\n  food: <CoffeeIcon aria-hidden=\"true\" />,\n  travel: <CarIcon aria-hidden=\"true\" />,\n  activities: <VolleyballIcon aria-hidden=\"true\" />,\n  objects: <LightbulbIcon aria-hidden=\"true\" />,\n  symbols: <HeartIcon aria-hidden=\"true\" />,\n  flags: <FlagIcon aria-hidden=\"true\" />,\n  custom: <SparklesIcon aria-hidden=\"true\" />,\n};\n\nconst TRIGGER_ICON = <SmilePlusIcon aria-hidden=\"true\" />;\n\nconst SEARCH_ICON = <SearchIcon aria-hidden=\"true\" />;\n\nconst CLEAR_ICON = <XIcon aria-hidden=\"true\" />;\n\n/* -------------------------------------------------------------------------- */\n/*                                   Context                                  */\n/* -------------------------------------------------------------------------- */\n\ntype ActiveCell = {\n  section: string;\n  id: string;\n  /* Arrow keys scroll the cell into view and keep Left and Right in the\n     grid; a hovered cell does neither. */\n  source: \"keyboard\" | \"pointer\";\n};\n\ntype Move = \"next\" | \"prev\" | \"up\" | \"down\" | \"pageUp\" | \"pageDown\" | \"first\" | \"last\" | \"visible\";\n\ntype EmojiPickerContextValue = {\n  id: string;\n  listId: string;\n  data: EmojiPickerData | null;\n  status: EmojiPickerDataStatus;\n  retry: () => void;\n  requestData: () => void;\n  emojis: EmojiPickerEmoji[];\n  i18n: EmojiPickerI18nConfig;\n  size: SizeTokens;\n  columns: number | \"auto\";\n  columnCount: number;\n  setMeasuredColumns: (columns: number) => void;\n  query: string;\n  setQuery: (query: string) => void;\n  skinTone: EmojiPickerSkinTone;\n  setSkinTone: (tone: EmojiPickerSkinTone) => void;\n  /** The tabs: every category with emoji, whatever the query. */\n  tabs: Section[];\n  /** Disabled stand-ins until the data lands, so the row does not jump. */\n  placeholderTabs: Section[];\n  /** What the grid shows: the tabs, or the search results. */\n  layout: Layout;\n  active: ActiveCell | null;\n  activePosition: { row: number; col: number } | undefined;\n  activeEmoji: EmojiPickerEmoji | null;\n  setActive: (cell: ActiveCell | null) => void;\n  activeSection: string | null;\n  setActiveSection: (id: string | null) => void;\n  jump: string | null;\n  setJump: (id: string | null) => void;\n  jumpTo: (id: string) => void;\n  setPageRows: (rows: number) => void;\n  navigate: (event: ReactKeyboardEvent<HTMLElement>, fromSearch: boolean) => void;\n  move: (to: Move) => void;\n  /** A scroll the keyboard did not make hands the highlight to the pointer. */\n  releaseKeyboard: () => void;\n  /** The search or the grid holds focus, so a keyboard highlight shows. */\n  focused: boolean;\n  setFocused: (focused: boolean) => void;\n  pick: (emoji: EmojiPickerEmoji, event?: { shiftKey?: boolean }) => void;\n  clear: () => void;\n  isSelected: (emoji: EmojiPickerEmoji) => boolean;\n  /** True when `max` is reached: unselected cells refuse a pick. */\n  full: boolean;\n  valueMode: boolean;\n  multiple: boolean;\n  values: string[];\n  findByValue: (value: string) => EmojiPickerEmoji | undefined;\n  renderEmoji?: (emoji: EmojiPickerEmoji, value: string) => ReactNode;\n  announcement: string;\n  disabled: boolean;\n  invalid: boolean;\n  label: string | undefined;\n  labelledBy: string | undefined;\n  describedBy: string | undefined;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** What the popover anchors to. */\n  triggerRef: RefObject<HTMLElement | null>;\n  hasSearch: boolean;\n  setHasSearch: (present: boolean) => void;\n  searchRef: RefObject<HTMLInputElement | null>;\n  listRef: RefObject<HTMLDivElement | null>;\n  registerFocusTarget: (node: HTMLElement | null) => void;\n};\n\nconst EmojiPickerContext = createContext<EmojiPickerContextValue | null>(null);\n\n/** \"inline\" unless a part renders inside `EmojiPickerContent`. */\nconst SurfaceContext = createContext<\"inline\" | \"popup\">(\"inline\");\n\nfunction useEmojiPickerContext(part: string) {\n  const context = useContext(EmojiPickerContext);\n  if (!context) throw new Error(`${part} must be used within an EmojiPicker`);\n  return context;\n}\n\ntype EmojiPickerApi = {\n  data: EmojiPickerData | null;\n  status: EmojiPickerDataStatus;\n  retry: () => void;\n  /** The emoji the grid can show: custom ones, then the data after version, flag and `filter` checks. */\n  emojis: EmojiPickerEmoji[];\n  query: string;\n  setQuery: (query: string) => void;\n  skinTone: EmojiPickerSkinTone;\n  setSkinTone: (tone: EmojiPickerSkinTone) => void;\n  /** The highlighted emoji: hovered, or reached with the arrow keys. */\n  activeEmoji: EmojiPickerEmoji | null;\n  /** Single mode: the value, or `null`. Multiple mode: the first value. */\n  value: string | null;\n  values: string[];\n  /** The recent section's emoji, in display order. */\n  recent: EmojiPickerEmoji[];\n  /** Same as clicking the emoji's cell. */\n  select: (emoji: EmojiPickerEmoji) => void;\n  /** Empties the value. */\n  clear: () => void;\n  isOpen: boolean;\n  setOpen: (isOpen: boolean) => void;\n};\n\n/* -------------------------------------------------------------------------- */\n/*                                    Root                                    */\n/* -------------------------------------------------------------------------- */\n\ntype EmojiPickerSingleProps = {\n  selectionMode?: \"single\";\n  /** Controlled value: a native emoji, or `:shortcode:` for a custom one. `null` is empty. */\n  value?: string | null;\n  defaultValue?: string | null;\n  onChange?: (value: string | null) => void;\n  max?: never;\n};\n\ntype EmojiPickerMultipleProps = {\n  /** Picks toggle in and out of a list of values, and the picker stays open. */\n  selectionMode: \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onChange?: (value: string[]) => void;\n  /** The most values a person can pick; cells refuse further picks at the cap. */\n  max?: number;\n};\n\ntype EmojiPickerProps = (EmojiPickerSingleProps | EmojiPickerMultipleProps) & {\n  /** Fires on every pick, with the toned value and the emoji. The insert-into-text hook. */\n  onEmojiSelect?: (selection: EmojiPickerSelection) => void;\n  /** Whether the popover is open (controlled). */\n  isOpen?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n  /** Closes the popover after a pick. Defaults to true, false with `selectionMode=\"multiple\"`. Shift+click keeps it open either way. */\n  closeOnSelect?: boolean;\n  /** Data already in hand (bundled, or loaded on the server). Skips the fetch. */\n  data?: EmojiPickerData;\n  /** An emojibase locale: names, keywords and category labels in that language. */\n  locale?: string;\n  /** Base URL of a self-hosted emojibase-data copy. */\n  dataUrl?: string;\n  /** Shortcode sets searched and shown. `false` skips the request. */\n  shortcodes?: EmojiPickerShortcodePreset | EmojiPickerShortcodePreset[] | false;\n  /** Hides emoji newer than this Emoji version. `\"auto\"` probes what the device draws, flags included. */\n  emojiVersion?: number | \"auto\";\n  /** Which sections show, in order: `recent`, custom category ids and category keys. Search still covers every emoji `filter` and `emojiVersion` keep; use `filter` to remove emoji entirely. */\n  categories?: string[];\n  /** Drops emoji. Pass a stable function; it runs per emoji. */\n  filter?: (emoji: EmojiPickerEmoji) => boolean;\n  customEmojis?: EmojiPickerCustomEmoji[];\n  customCategories?: EmojiPickerCustomCategory[];\n  /** Orders the recent section by use count, or by last use. */\n  recentMode?: \"frequent\" | \"recent\";\n  /** How many recent emoji show. Defaults to 2 rows; 0 hides the section. */\n  recentLimit?: number;\n  /** Controlled recent list, for syncing it to an account. */\n  recent?: EmojiPickerRecentEntry[];\n  defaultRecent?: EmojiPickerRecentEntry[];\n  onRecentChange?: (recent: EmojiPickerRecentEntry[]) => void;\n  /** localStorage key for the recent list; `null` keeps it in memory. */\n  recentStorageKey?: string | null;\n  skinTone?: EmojiPickerSkinTone;\n  defaultSkinTone?: EmojiPickerSkinTone;\n  onSkinToneChange?: (tone: EmojiPickerSkinTone) => void;\n  /** localStorage key for the skin tone; `null` keeps it in memory. */\n  skinToneStorageKey?: string | null;\n  /** Controlled search text. */\n  query?: string;\n  onQueryChange?: (query: string) => void;\n  size?: EmojiPickerSize;\n  /** Cells per row. `\"auto\"` fills the panel's width. */\n  columns?: number | \"auto\";\n  /** Draws an emoji: an image set, a sprite, a brand font. */\n  renderEmoji?: (emoji: EmojiPickerEmoji, value: string) => ReactNode;\n  isDisabled?: boolean;\n  isInvalid?: boolean;\n  /** Submits the value with a native form; multiple values are comma-joined. */\n  name?: string;\n  form?: string;\n  /** Fails native form validation while empty. */\n  isRequired?: boolean;\n  /** The hidden form field, for form libraries that focus a field on error. */\n  inputRef?: Ref<HTMLInputElement>;\n  /** Lands on the trigger. */\n  id?: string;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  \"aria-describedby\"?: string;\n  i18n?: EmojiPickerI18nOverrides;\n  /** Without children the picker renders a trigger and a popover; this reaches the trigger. */\n  className?: string;\n  children?: ReactNode;\n};\n\nconst EMPTY_RECENT: EmojiPickerRecentEntry[] = [];\n\nconst toValues = (value: string | string[] | null | undefined) =>\n  Array.isArray(value) ? value : value ? [value] : [];\n\nfunction assignRef<T>(ref: Ref<T> | undefined, node: T | null) {\n  if (typeof ref === \"function\") ref(node);\n  else if (ref) ref.current = node;\n}\n\nfunction normalizeCustom(emoji: EmojiPickerCustomEmoji): EmojiPickerEmoji {\n  return {\n    id: emoji.id,\n    emoji: \"\",\n    label: emoji.label,\n    tags: emoji.tags ?? [],\n    shortcodes: emoji.shortcodes?.length ? emoji.shortcodes : [emoji.id],\n    category: emoji.category ?? \"custom\",\n    version: 0,\n    src: emoji.src,\n  };\n}\n\nfunction EmojiPicker(props: EmojiPickerProps) {\n  const {\n    onEmojiSelect,\n    isOpen: openProp,\n    defaultOpen = false,\n    onOpenChange,\n    data: dataProp,\n    locale = \"en\",\n    dataUrl,\n    shortcodes = \"github\",\n    emojiVersion = \"auto\",\n    categories,\n    filter,\n    customEmojis,\n    customCategories,\n    recentMode = \"frequent\",\n    recentLimit,\n    recent: recentProp,\n    defaultRecent,\n    onRecentChange,\n    recentStorageKey = \"fma-ds-emoji-picker-recent\",\n    skinTone: skinToneProp,\n    defaultSkinTone = 0,\n    onSkinToneChange,\n    skinToneStorageKey = \"fma-ds-emoji-picker-skin-tone\",\n    query: queryProp,\n    onQueryChange,\n    size = \"default\",\n    columns = 8,\n    renderEmoji,\n    isDisabled: disabled = false,\n    isInvalid: invalid = false,\n    name,\n    form,\n    isRequired: required = false,\n    inputRef,\n    id: idProp,\n    \"aria-label\": label,\n    \"aria-labelledby\": labelledBy,\n    \"aria-describedby\": describedBy,\n    i18n: i18nProp,\n    className,\n    children,\n  } = props;\n  const multiple = props.selectionMode === \"multiple\";\n  const max = props.selectionMode === \"multiple\" ? props.max : undefined;\n  const closeOnSelect = props.closeOnSelect ?? !multiple;\n  const generatedId = useId();\n  const id = idProp ?? generatedId;\n  const listId = `${id}-list`;\n  const tokens = SIZES[size];\n\n  const [uncontrolled, setUncontrolled] = useState(() => toValues(props.defaultValue));\n  /* What a form reset restores, as a native field returns to its default. */\n  const [initialValues] = useState(uncontrolled);\n  const values = props.value !== undefined ? toValues(props.value) : uncontrolled;\n  const valueMode =\n    props.value !== undefined || props.defaultValue !== undefined || multiple || !!name;\n\n  const [openState, setOpenState] = useState(defaultOpen);\n  const open = openProp ?? openState;\n  const [queryState, setQueryState] = useState(\"\");\n  const query = queryProp ?? queryState;\n  const [active, setActive] = useState<ActiveCell | null>(null);\n  const [focused, setFocused] = useState(false);\n  const [activeSection, setActiveSection] = useState<string | null>(null);\n  const [jump, setJump] = useState<string | null>(null);\n  const [measuredColumns, setMeasuredColumns] = useState<number | null>(null);\n  const [hasSearch, setHasSearch] = useState(false);\n  const [wanted, setWanted] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  /* Every open and close starts the grid over: no stale search, no stale highlight. */\n  const [seenOpen, setSeenOpen] = useState(open);\n  /* The recent list as it was before this session's first pick. The section\n     shows it until the next open (an inline panel, the next mount), so a pick\n     never inserts a section and moves the grid under the pointer. */\n  const [pinnedRecent, setPinnedRecent] = useState<EmojiPickerRecentEntry[] | null>(null);\n  if (seenOpen !== open) {\n    setSeenOpen(open);\n    setQueryState(\"\");\n    setActive(null);\n    setFocused(false);\n    setJump(null);\n    setAnnouncement(\"\");\n    setPinnedRecent(null);\n  }\n  /* A language switch drops the last announcement, so the live region never\n     holds text in the previous language. */\n  const [seenLocale, setSeenLocale] = useState(locale);\n  if (seenLocale !== locale) {\n    setSeenLocale(locale);\n    setAnnouncement(\"\");\n  }\n  const searchRef = useRef<HTMLInputElement | null>(null);\n  const listRef = useRef<HTMLDivElement | null>(null);\n  const focusTarget = useRef<HTMLElement | null>(null);\n  const triggerRef = useRef<HTMLElement | null>(null);\n  const fieldRef = useRef<HTMLInputElement | null>(null);\n  const resetRef = useRef(() => {});\n  const pageRows = useRef(VISIBLE_ROWS);\n  /* The column Up and Down aim for, kept across a shorter row. */\n  const goalColumn = useRef<number | null>(null);\n\n  const requestData = useCallback(() => setWanted(true), []);\n  const { data, status, retry } = useEmojiPickerData({\n    data: dataProp,\n    locale,\n    url: dataUrl,\n    shortcodes,\n    enabled: wanted || open,\n  });\n  const support = useEmojiSupport(data, emojiVersion);\n  const i18n = useMemo(() => mergeEmojiPickerI18n(i18nProp), [i18nProp]);\n  const { labels } = i18n;\n  const columnCount = columns === \"auto\" ? (measuredColumns ?? 8) : Math.max(1, columns);\n\n  const unicode = useMemo(() => {\n    if (!data) return [];\n    return data.emojis.flatMap((emoji) => {\n      if (emoji.version > support.version) return [];\n      if (!support.flags && isRegionalFlag(emoji.emoji)) return [];\n      if (filter && !filter(emoji)) return [];\n      return emoji.skins && (emoji.skinVersion ?? 0) > support.version\n        ? [{ ...emoji, skins: undefined, skinVersion: undefined }]\n        : [emoji];\n    });\n  }, [data, support, filter]);\n  const custom = useMemo(\n    () => (customEmojis ?? []).map(normalizeCustom).filter((emoji) => !filter || filter(emoji)),\n    [customEmojis, filter],\n  );\n  const all = useMemo(() => [...custom, ...unicode], [custom, unicode]);\n  const byId = useMemo(() => new Map(all.map((emoji) => [emoji.id, emoji])), [all]);\n  /* Every value the data can produce, filtered or not, for display. */\n  const byValue = useMemo(() => {\n    const map = new Map<string, EmojiPickerEmoji>();\n    for (const emoji of [...custom, ...(data?.emojis ?? [])]) {\n      map.set(bare(getEmojiValue(emoji)), emoji);\n      for (const skin of emoji.skins ?? []) map.set(bare(skin), emoji);\n    }\n    return map;\n  }, [custom, data]);\n\n  const storedRecent = useStored(recentProp === undefined ? recentStorageKey : null);\n  const parsedRecent = useMemo(() => parseRecent(storedRecent), [storedRecent]);\n  const [memoryRecent, setMemoryRecent] = useState(defaultRecent ?? EMPTY_RECENT);\n  const recentEntries =\n    recentProp ??\n    (recentStorageKey ? (parsedRecent ?? defaultRecent ?? EMPTY_RECENT) : memoryRecent);\n  const shownRecent = recentLimit ?? columnCount * 2;\n  const shownEntries = pinnedRecent ?? recentEntries;\n  const recent = useMemo(() => {\n    const ordered =\n      recentMode === \"frequent\"\n        ? [...shownEntries].sort((a, b) => b.count - a.count)\n        : shownEntries;\n    return ordered.flatMap((entry) => byId.get(entry.id) ?? []).slice(0, shownRecent);\n  }, [shownEntries, recentMode, byId, shownRecent]);\n\n  const storedTone = useStored(skinToneProp === undefined ? skinToneStorageKey : null);\n  const [memoryTone, setMemoryTone] = useState(defaultSkinTone);\n  const skinTone =\n    skinToneProp ?? (skinToneStorageKey ? (parseTone(storedTone) ?? defaultSkinTone) : memoryTone);\n\n  const tabs = useMemo(() => {\n    const customIds = [\n      ...new Set([\n        ...(customCategories ?? []).map((category) => category.id),\n        ...custom.map((emoji) => emoji.category),\n      ]),\n    ];\n    const order = categories ?? [\"recent\", ...customIds, ...EMOJI_PICKER_CATEGORIES];\n    const grouped = new Map<string, EmojiPickerEmoji[]>();\n    for (const emoji of all) {\n      const list = grouped.get(emoji.category);\n      if (list) list.push(emoji);\n      else grouped.set(emoji.category, [emoji]);\n    }\n    const sections: Section[] = [];\n    for (const key of new Set(order)) {\n      const emojis = key === \"recent\" ? recent : grouped.get(key);\n      if (!emojis?.length) continue;\n      const customCategory = customCategories?.find((item) => item.id === key);\n      sections.push({\n        id: key,\n        label:\n          key === \"recent\"\n            ? labels[recentMode]\n            : (customCategory?.label ??\n              categoryLabel(key, labels, i18nProp, data) ??\n              labels.custom),\n        emojis,\n        icon: customCategory?.icon ?? CATEGORY_ICONS[key] ?? CATEGORY_ICONS.custom,\n      });\n    }\n    return sections;\n  }, [all, recent, categories, customCategories, custom, labels, recentMode, i18nProp, data]);\n\n  const placeholderTabs = useMemo(\n    () =>\n      (categories ?? EMOJI_PICKER_CATEGORIES).flatMap((key) =>\n        (EMOJI_PICKER_CATEGORIES as string[]).includes(key)\n          ? [\n              {\n                id: key,\n                label: labels[key as EmojiPickerCategory],\n                emojis: [],\n                icon: CATEGORY_ICONS[key],\n              },\n            ]\n          : [],\n      ),\n    [categories, labels],\n  );\n\n  const results = useMemo(() => (query.trim() ? searchEmojis(all, query) : null), [all, query]);\n  const sections = useMemo<Section[]>(\n    () =>\n      results\n        ? results.length\n          ? [\n              {\n                id: \"search\",\n                label: labels.searchResults,\n                emojis: results,\n                icon: null,\n              },\n            ]\n          : []\n        : tabs,\n    [results, tabs, labels],\n  );\n  const layout = useMemo(\n    () => buildLayout(sections, columnCount, tokens),\n    [sections, columnCount, tokens],\n  );\n\n  /* With a query and nothing chosen yet, the first result is highlighted, so\n     Enter picks the best match. */\n  let activePosition = active\n    ? layout.positions.get(cellKey(active.section, active.id))\n    : undefined;\n  if (!activePosition && results && layout.rows.length) {\n    activePosition = { row: 0, col: 0 };\n  }\n  const activeEmoji = activePosition\n    ? layout.rows[activePosition.row].cells[activePosition.col]\n    : null;\n\n  const selected = useMemo(() => new Set(values.map(bare)), [values]);\n  const isSelected = (emoji: EmojiPickerEmoji) =>\n    selected.has(bare(getEmojiValue(emoji))) ||\n    !!emoji.skins?.some((skin) => selected.has(bare(skin)));\n  const full = multiple && max !== undefined && values.length >= max;\n\n  const setOpen = (next: boolean) => {\n    if (next && disabled) return;\n    if (openProp === undefined) setOpenState(next);\n    onOpenChange?.(next);\n  };\n\n  const setQuery = (next: string) => {\n    if (queryProp === undefined) setQueryState(next);\n    onQueryChange?.(next);\n    setActive(null);\n    setAnnouncement(\"\");\n  };\n\n  const setSkinTone = (tone: EmojiPickerSkinTone) => {\n    if (skinToneProp === undefined) {\n      if (skinToneStorageKey) writeStored(skinToneStorageKey, String(tone));\n      else setMemoryTone(tone);\n    }\n    onSkinToneChange?.(tone);\n  };\n\n  const commitValues = (next: string[]) => {\n    if (next.length === values.length && next.every((v, i) => v === values[i])) {\n      return;\n    }\n    if (props.value === undefined) setUncontrolled(next);\n    if (props.selectionMode === \"multiple\") props.onChange?.(next);\n    else props.onChange?.(next[0] ?? null);\n  };\n\n  const recordRecent = (emojiId: string) => {\n    if (pinnedRecent === null) setPinnedRecent(recentEntries);\n    const previous = recentEntries.find((entry) => entry.id === emojiId);\n    const next = [\n      { id: emojiId, count: (previous?.count ?? 0) + 1 },\n      ...recentEntries.filter((entry) => entry.id !== emojiId),\n    ].slice(0, RECENT_CAP);\n    if (recentProp === undefined) {\n      if (recentStorageKey) writeStored(recentStorageKey, JSON.stringify(next));\n      else setMemoryRecent(next);\n    }\n    onRecentChange?.(next);\n  };\n\n  const pick = (emoji: EmojiPickerEmoji, event?: { shiftKey?: boolean }) => {\n    if (disabled) return;\n    const tone = emoji.skins ? skinTone : 0;\n    const value = getEmojiValue(emoji, tone);\n    if (multiple && isSelected(emoji)) {\n      commitValues(values.filter((item) => !isEmojiValue(emoji, item)));\n      return;\n    }\n    if (full) return;\n    commitValues(multiple ? [...values, value] : [value]);\n    onEmojiSelect?.({ value, emoji, skinTone: tone });\n    recordRecent(emoji.id);\n    const close = closeOnSelect && !event?.shiftKey && open;\n    if (close) setOpen(false);\n    else setAnnouncement(i18n.functions.formatSelected(i18n.functions.formatLabel(emoji.label)));\n  };\n\n  const move = (to: Move) => {\n    const { rows } = layout;\n    if (rows.length === 0) return;\n    const at = activePosition;\n    let row = 0;\n    let col = 0;\n    /* With nothing highlighted, the keyboard starts at the first row in view\n       under the sticky header, so it picks up after a category jump. */\n    if (!at && to !== \"first\" && to !== \"last\") to = \"visible\";\n    if (to === \"visible\") {\n      const offset = (listRef.current?.scrollTop ?? 0) + tokens.header - 1;\n      row = indexAt(rows, offset);\n      if (rows[row].top < offset && row + 1 < rows.length) row++;\n      goalColumn.current = null;\n    } else if (at && to !== \"first\" && to !== \"last\") {\n      row = at.row;\n      col = at.col;\n      const vertical = to === \"up\" || to === \"down\" || to === \"pageUp\" || to === \"pageDown\";\n      if (vertical) col = goalColumn.current ?? col;\n      if (to === \"next\") {\n        if (col + 1 < rows[row].cells.length) col++;\n        else if (row + 1 < rows.length) [row, col] = [row + 1, 0];\n      } else if (to === \"prev\") {\n        if (col > 0) col--;\n        else if (row > 0) [row, col] = [row - 1, rows[row - 1].cells.length - 1];\n      } else if (to === \"down\") row = Math.min(row + 1, rows.length - 1);\n      else if (to === \"pageDown\") row = Math.min(row + pageRows.current, rows.length - 1);\n      else if (to === \"pageUp\") row = Math.max(row - pageRows.current, 0);\n      else if (to === \"up\") {\n        /* Up from the top row hands the arrows back to the search caret. */\n        if (row === 0 && !results) {\n          setActive(null);\n          return;\n        }\n        row = Math.max(row - 1, 0);\n      }\n      goalColumn.current = vertical ? col : null;\n      col = Math.min(col, rows[row].cells.length - 1);\n    } else if (to === \"last\") {\n      row = rows.length - 1;\n      col = rows[row].cells.length - 1;\n      goalColumn.current = null;\n    } else {\n      goalColumn.current = null;\n    }\n    const cell = rows[row].cells[col];\n    setActive({\n      section: layout.sections[rows[row].section].section.id,\n      id: cell.id,\n      source: \"keyboard\",\n    });\n  };\n\n  const navigate = (event: ReactKeyboardEvent<HTMLElement>, fromSearch: boolean) => {\n    if (event.nativeEvent.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;\n    /* Left and Right belong to the caret until the arrows enter the grid. */\n    const inGrid = !fromSearch || active?.source === \"keyboard\";\n    const rtl = getComputedStyle(event.currentTarget).direction === \"rtl\";\n    const run = (to: Move) => {\n      event.preventDefault();\n      move(to);\n    };\n    switch (event.key) {\n      case \"ArrowDown\":\n        return run(activePosition ? \"down\" : \"visible\");\n      case \"ArrowUp\":\n        if (activePosition) return run(\"up\");\n        return fromSearch ? undefined : run(\"visible\");\n      case \"ArrowRight\":\n      case \"ArrowLeft\":\n        if (!inGrid) return;\n        return run((event.key === \"ArrowRight\") !== rtl ? \"next\" : \"prev\");\n      case \"PageDown\":\n        return run(\"pageDown\");\n      case \"PageUp\":\n        return run(\"pageUp\");\n      case \"Home\":\n      case \"End\":\n        if (fromSearch) return;\n        return run(event.key === \"Home\" ? \"first\" : \"last\");\n      case \"Enter\":\n      case \" \":\n        if (event.key === \" \" && fromSearch) return;\n        if (!activeEmoji) return;\n        event.preventDefault();\n        return pick(activeEmoji, event);\n    }\n  };\n\n  const context: EmojiPickerContextValue = {\n    id,\n    listId,\n    data,\n    status,\n    retry,\n    requestData,\n    emojis: all,\n    i18n,\n    size: tokens,\n    columns,\n    columnCount,\n    setMeasuredColumns,\n    query,\n    setQuery,\n    skinTone,\n    setSkinTone,\n    tabs,\n    placeholderTabs,\n    layout,\n    active,\n    activePosition,\n    activeEmoji,\n    setActive: (cell) => {\n      goalColumn.current = null;\n      setActive(cell);\n    },\n    activeSection,\n    setActiveSection,\n    jump,\n    setJump,\n    jumpTo: (sectionId) => {\n      if (results) setQuery(\"\");\n      setActive(null);\n      setJump(sectionId);\n    },\n    setPageRows: (rows) => {\n      pageRows.current = rows;\n    },\n    navigate,\n    move,\n    releaseKeyboard: () =>\n      setActive((current) =>\n        current?.source === \"keyboard\" ? { ...current, source: \"pointer\" } : current,\n      ),\n    focused,\n    setFocused,\n    pick,\n    clear: () => commitValues([]),\n    isSelected,\n    full,\n    valueMode,\n    multiple,\n    values,\n    findByValue: (value) => byValue.get(bare(value)),\n    renderEmoji,\n    announcement,\n    disabled,\n    invalid,\n    label,\n    labelledBy,\n    describedBy,\n    open,\n    setOpen,\n    triggerRef,\n    hasSearch,\n    setHasSearch,\n    searchRef,\n    listRef,\n    registerFocusTarget: (node) => {\n      if (node) focusTarget.current = node;\n    },\n  };\n\n  useEffect(() => {\n    resetRef.current = () => commitValues(initialValues);\n  });\n\n  /* Listens on the field's own root and asks for its form at reset time, so a\n     `form` attribute is honoured. The restore waits a task, like a native\n     reset's default action, because any listener may still cancel it. */\n  const hasField = !!name || required;\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-attach when the field mounts or unmounts\n  useEffect(() => {\n    const root = fieldRef.current?.getRootNode();\n    if (!root) return;\n    let pending = 0;\n    const onReset = (event: Event) => {\n      if (event.target !== fieldRef.current?.form) return;\n      pending = window.setTimeout(() => {\n        if (!event.defaultPrevented) resetRef.current();\n      });\n    };\n    root.addEventListener(\"reset\", onReset, true);\n    return () => {\n      root.removeEventListener(\"reset\", onReset, true);\n      window.clearTimeout(pending);\n    };\n  }, [hasField]);\n\n  return (\n    <EmojiPickerContext.Provider value={context}>\n      {children === undefined ? (\n        <>\n          <EmojiPickerTrigger className={className} />\n          <EmojiPickerContent />\n        </>\n      ) : (\n        children\n      )}\n      {hasField && (\n        <input\n          ref={(node) => {\n            fieldRef.current = node;\n            assignRef(inputRef, node);\n          }}\n          type=\"text\"\n          name={name}\n          form={form}\n          required={required}\n          disabled={disabled}\n          value={values.join(\",\")}\n          onChange={() => {}}\n          tabIndex={-1}\n          aria-hidden=\"true\"\n          data-slot=\"emoji-picker-field\"\n          /* A failed `isRequired` check focuses this field; hand that to the control. */\n          onFocus={(event) => {\n            const target = focusTarget.current ?? searchRef.current;\n            if (target) target.focus();\n            else event.currentTarget.blur();\n          }}\n          className=\"pointer-events-none absolute size-px max-w-px opacity-0\"\n        />\n      )}\n    </EmojiPickerContext.Provider>\n  );\n}\n\n/* An explicit label wins, then the data's own language, then English. */\nfunction categoryLabel(\n  key: string,\n  labels: EmojiPickerLabels,\n  overrides: EmojiPickerI18nOverrides | undefined,\n  data: EmojiPickerData | null,\n) {\n  if (!(EMOJI_PICKER_CATEGORIES as string[]).includes(key)) return undefined;\n  const category = key as EmojiPickerCategory;\n  return (\n    overrides?.labels?.[category] ??\n    (data && data.locale !== \"en\" ? data.categories[category] : undefined) ??\n    labels[category]\n  );\n}\n\n/** The picker's state and actions, inside an `EmojiPicker`. */\nfunction useEmojiPicker(): EmojiPickerApi {\n  const context = useEmojiPickerContext(\"useEmojiPicker\");\n  return {\n    data: context.data,\n    status: context.status,\n    retry: context.retry,\n    emojis: context.emojis,\n    query: context.query,\n    setQuery: context.setQuery,\n    skinTone: context.skinTone,\n    setSkinTone: context.setSkinTone,\n    activeEmoji: context.activeEmoji,\n    value: context.values[0] ?? null,\n    values: context.values,\n    recent: context.tabs.find((section) => section.id === \"recent\")?.emojis ?? [],\n    select: (emoji) => context.pick(emoji),\n    clear: context.clear,\n    isOpen: context.open,\n    setOpen: context.setOpen,\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Glyph                                   */\n/* -------------------------------------------------------------------------- */\n\n/* The emoji font stack, swappable through `--emoji-picker-font` for a\n   self-hosted color font. */\nconst EMOJI_FONT_CLASS =\n  \"font-[family-name:var(--emoji-picker-font,Apple_Color_Emoji,Twemoji_Mozilla,Segoe_UI_Emoji,Segoe_UI_Symbol,Noto_Color_Emoji,sans-serif)] leading-none\";\n\nfunction EmojiGlyph({\n  emoji,\n  value,\n  className,\n}: {\n  emoji: EmojiPickerEmoji | undefined | null;\n  value: string;\n  className?: string;\n}) {\n  const context = useContext(EmojiPickerContext);\n  if (emoji && context?.renderEmoji) return <>{context.renderEmoji(emoji, value)}</>;\n  if (emoji?.src) {\n    return (\n      <img\n        src={emoji.src}\n        alt=\"\"\n        width={64}\n        height={64}\n        loading=\"lazy\"\n        decoding=\"async\"\n        draggable={false}\n        className={cn(\"size-[1em] object-contain\", className)}\n      />\n    );\n  }\n  return <span className={cn(EMOJI_FONT_CLASS, className)}>{value}</span>;\n}\n\ntype EmojiPickerValueProps = Omit<ComponentProps<\"span\">, \"children\"> & {\n  /** Defaults to the picker's value. */\n  value?: string | null;\n  placeholder?: ReactNode;\n};\n\n/** An emoji value drawn like the grid draws it, custom images included. For triggers and readouts. */\nfunction EmojiPickerValue({\n  value: valueProp,\n  placeholder,\n  className,\n  ...props\n}: EmojiPickerValueProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerValue\");\n  const value = valueProp === undefined ? (context.values[0] ?? null) : valueProp;\n  return (\n    <span\n      data-slot=\"emoji-picker-value\"\n      data-placeholder={value ? undefined : \"\"}\n      className={cn(\"inline-flex items-center justify-center\", className)}\n      {...props}\n    >\n      {value ? <EmojiGlyph emoji={context.findByValue(value)} value={value} /> : placeholder}\n    </span>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Trigger                                  */\n/* -------------------------------------------------------------------------- */\n\ntype EmojiPickerTriggerProps = Omit<ComponentProps<typeof Button>, \"children\"> & {\n  /** Replaces the value or the smiley. */\n  children?: ReactNode;\n};\n\n/**\n * A Button that opens the picker: the value, or a smiley. Hovering or\n * focusing it starts the data fetch, so the grid is ready on open.\n */\nfunction EmojiPickerTrigger({\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  children,\n  ref,\n  onPress,\n  onHoverStart,\n  onFocus,\n  ...props\n}: EmojiPickerTriggerProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerTrigger\");\n  const value = context.multiple ? null : (context.values[0] ?? null);\n  const emoji = value ? context.findByValue(value) : undefined;\n  const name = context.label ?? context.i18n.labels.trigger;\n\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      isDisabled={context.disabled}\n      /* Before the data loads, the glyph itself stands in for its name: screen\n         readers speak an emoji's name. */\n      aria-label={\n        context.labelledBy\n          ? undefined\n          : emoji\n            ? `${name}: ${context.i18n.functions.formatLabel(emoji.label)}`\n            : value\n              ? `${name}: ${value}`\n              : name\n      }\n      aria-labelledby={context.labelledBy}\n      aria-describedby={context.describedBy}\n      aria-haspopup=\"dialog\"\n      aria-expanded={context.open}\n      aria-invalid={context.invalid || undefined}\n      data-invalid={context.invalid ? \"\" : undefined}\n      {...props}\n      id={context.id}\n      ref={(node: HTMLButtonElement | null) => {\n        context.triggerRef.current = node;\n        context.registerFocusTarget(node);\n        assignRef(ref, node);\n      }}\n      onPress={(event) => {\n        onPress?.(event);\n        context.setOpen(!context.open);\n      }}\n      onHoverStart={(event) => {\n        onHoverStart?.(event);\n        context.requestData();\n      }}\n      onFocus={(event) => {\n        onFocus?.(event);\n        context.requestData();\n      }}\n      data-slot=\"emoji-picker-trigger\"\n      data-placeholder={value ? undefined : \"\"}\n      /* The value glyph reads larger than label text; a className size wins. */\n      className={cn(value && children === undefined && \"text-lg\", className)}\n    >\n      {children === undefined ? value ? <EmojiPickerValue /> : TRIGGER_ICON : children}\n    </Button>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                Content, Panel                              */\n/* -------------------------------------------------------------------------- */\n\nconst isTouch = () =>\n  typeof window !== \"undefined\" && !!window.matchMedia?.(\"(pointer: coarse)\").matches;\n\ntype EmojiPickerContentProps = Omit<\n  ComponentProps<typeof Popover>,\n  \"children\" | \"isOpen\" | \"onOpenChange\" | \"triggerRef\"\n> & {\n  /** Where focus goes on close, such as a composer's textarea. Defaults to the trigger. */\n  returnFocus?: RefObject<HTMLElement | null>;\n  /** The panel's parts; without them, the default layout. */\n  children?: ReactNode;\n};\n\n/**\n * The popover: a modal react-aria `Popover` around a `Dialog`, so Tab cycles\n * inside it, Escape and an outside press close it, and focus returns to the\n * trigger (or `returnFocus`). It wraps its children in an `EmojiPickerPanel`,\n * so it takes the same parts; with none it renders the default layout.\n */\nfunction EmojiPickerContent({\n  className,\n  placement = \"bottom start\",\n  returnFocus,\n  children,\n  ...props\n}: EmojiPickerContentProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerContent\");\n  return (\n    <Popover\n      placement={placement}\n      triggerRef={context.triggerRef}\n      isOpen={context.open}\n      onOpenChange={context.setOpen}\n      /* The grid gives up rows first when the popover is short on room. */\n      className={cn(\"w-auto gap-0 overflow-hidden p-0\", className)}\n      {...props}\n    >\n      <Dialog\n        aria-labelledby={context.labelledBy}\n        aria-label={context.labelledBy ? undefined : (context.label ?? context.i18n.labels.panel)}\n        className=\"flex max-h-[inherit] min-h-0 flex-col outline-none\"\n      >\n        <SurfaceContext.Provider value=\"popup\">\n          <EmojiPickerPanel\n            /* React bubbles a portaled click through the component tree, so a\n               popover inside an InputGroupAddon would see every click in here\n               and move focus to the group's input, out of the search. */\n            onClick={(event) => event.stopPropagation()}\n          >\n            {children}\n          </EmojiPickerPanel>\n          <FocusOnOpen returnFocus={returnFocus} />\n        </SurfaceContext.Provider>\n      </Dialog>\n    </Popover>\n  );\n}\n\n/* Focuses the search on open, or the grid on a touch screen, where the search\n   would raise the keyboard over the grid. Child effects run before the\n   Dialog's own, which then leaves focus where it is. */\nfunction FocusOnOpen({ returnFocus }: { returnFocus?: RefObject<HTMLElement | null> }) {\n  const { searchRef, listRef } = useEmojiPickerContext(\"EmojiPickerContent\");\n  useEffect(() => {\n    const target = (isTouch() ? null : searchRef.current) ?? listRef.current;\n    target?.focus({ preventScroll: true });\n    return () => {\n      const element = returnFocus?.current;\n      /* After react-aria hands focus back to the trigger. */\n      if (element) requestAnimationFrame(() => element.focus());\n    };\n  }, [searchRef, listRef, returnFocus]);\n  return null;\n}\n\ntype EmojiPickerPanelProps = ComponentProps<\"div\">;\n\n/**\n * The picker surface. Inline, put it straight inside an `EmojiPicker` with no\n * trigger. Without children it renders the search, the tabs, the grid and a\n * footer with the preview and the skin tone; with children, only those parts,\n * in that order.\n */\nfunction EmojiPickerPanel({ className, style, children, ...props }: EmojiPickerPanelProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerPanel\");\n  const inline = useContext(SurfaceContext) === \"inline\";\n  const { requestData } = context;\n  const { cell, emoji, header, gutter } = context.size;\n\n  useEffect(() => {\n    requestData();\n  }, [requestData]);\n\n  return (\n    <div\n      {...(inline && {\n        role: \"group\",\n        \"aria-labelledby\": context.labelledBy,\n        \"aria-label\": context.labelledBy ? undefined : (context.label ?? context.i18n.labels.panel),\n      })}\n      data-slot=\"emoji-picker-panel\"\n      data-columns={context.columns === \"auto\" ? \"auto\" : undefined}\n      data-disabled={context.disabled ? \"\" : undefined}\n      style={\n        {\n          \"--emoji-picker-cell\": `${cell}px`,\n          \"--emoji-picker-emoji\": `${emoji}px`,\n          \"--emoji-picker-header\": `${header}px`,\n          \"--emoji-picker-gutter\": `${gutter}px`,\n          \"--emoji-picker-padding\": `${PADDING}px`,\n          \"--emoji-picker-columns\": context.columnCount,\n          \"--emoji-picker-list-height\": `${header + VISIBLE_ROWS * cell + gutter}px`,\n          ...style,\n        } as CSSProperties\n      }\n      className={cn(\n        /* Sized from the columns: the list hides its scrollbar, so nothing\n           else takes width. */\n        \"flex min-h-0 w-[calc(var(--emoji-picker-columns)*var(--emoji-picker-cell)+var(--emoji-picker-gutter)*2)] max-w-full min-w-0 flex-col data-disabled:pointer-events-none data-disabled:opacity-50 data-[columns=auto]:w-full\",\n        /* The sticky headers paint the surface they sit on: an inline panel\n           belongs in a Card, a popup is the popover. */\n        inline\n          ? \"[--emoji-picker-surface:var(--color-card)]\"\n          : \"[--emoji-picker-surface:var(--color-popover)]\",\n        className,\n      )}\n      {...props}\n    >\n      {children === undefined ? (\n        <>\n          <EmojiPickerHeader>\n            <EmojiPickerSearch />\n          </EmojiPickerHeader>\n          <EmojiPickerCategories />\n          <EmojiPickerList />\n          <EmojiPickerFooter>\n            <EmojiPickerPreview />\n            <EmojiPickerSkinTone />\n          </EmojiPickerFooter>\n        </>\n      ) : (\n        children\n      )}\n    </div>\n  );\n}\n\ntype EmojiPickerHeaderProps = ComponentProps<\"div\">;\n\n/** The row above the grid, for the search and anything beside it. */\nfunction EmojiPickerHeader({ className, ...props }: EmojiPickerHeaderProps) {\n  return (\n    <div\n      data-slot=\"emoji-picker-header\"\n      className={cn(\n        \"flex items-center gap-2 px-(--emoji-picker-padding) pt-(--emoji-picker-padding) pb-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Search                                   */\n/* -------------------------------------------------------------------------- */\n\ntype EmojiPickerSearchProps = Omit<\n  ComponentProps<typeof InputGroupInput>,\n  \"value\" | \"defaultValue\"\n>;\n\n/**\n * The search field, an InputGroup. Focus stays here while the\n * arrow keys move through the grid (a combobox over the listbox); Enter picks\n * the highlighted emoji.\n */\nfunction EmojiPickerSearch({\n  className,\n  placeholder,\n  onChange,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: EmojiPickerSearchProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerSearch\");\n  const { setHasSearch, searchRef } = context;\n  const { labels } = context.i18n;\n  const activeId = context.activePosition\n    ? optionId(context.listId, context.activePosition)\n    : undefined;\n\n  useEffect(() => {\n    setHasSearch(true);\n    return () => setHasSearch(false);\n  }, [setHasSearch]);\n\n  return (\n    <InputGroup data-slot=\"emoji-picker-search\" className={cn(\"flex-1\", className)}>\n      <InputGroupAddon>{SEARCH_ICON}</InputGroupAddon>\n      <InputGroupInput\n        type=\"text\"\n        role=\"combobox\"\n        aria-expanded=\"true\"\n        aria-controls={context.listId}\n        aria-activedescendant={activeId}\n        aria-autocomplete=\"list\"\n        aria-label={labels.search}\n        placeholder={placeholder ?? labels.search}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck={false}\n        enterKeyHint=\"search\"\n        disabled={context.disabled}\n        value={context.query}\n        onChange={(event) => {\n          onChange?.(event);\n          context.setQuery(event.target.value);\n        }}\n        onFocus={(event) => {\n          onFocus?.(event);\n          context.setFocused(true);\n        }}\n        onBlur={(event) => {\n          onBlur?.(event);\n          context.setFocused(false);\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented) return;\n          /* In a popover Escape closes it; inline it clears the search. */\n          if (event.key === \"Escape\" && context.query && !context.open) {\n            event.preventDefault();\n            context.setQuery(\"\");\n            return;\n          }\n          context.navigate(event, true);\n        }}\n        {...props}\n        ref={(node: HTMLInputElement | null) => {\n          searchRef.current = node;\n          assignRef(ref, node);\n        }}\n      />\n      {context.query && (\n        <InputGroupAddon align=\"inline-end\">\n          <InputGroupButton\n            size=\"icon-xs\"\n            aria-label={labels.clearSearch}\n            data-slot=\"emoji-picker-search-clear\"\n            onPress={() => {\n              context.setQuery(\"\");\n              searchRef.current?.focus();\n            }}\n          >\n            {CLEAR_ICON}\n          </InputGroupButton>\n        </InputGroupAddon>\n      )}\n    </InputGroup>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Skin tone                                 */\n/* -------------------------------------------------------------------------- */\n\nconst HAND = \"✋\";\nconst TONE_MODIFIERS = [\"\", \"\\u{1F3FB}\", \"\\u{1F3FC}\", \"\\u{1F3FD}\", \"\\u{1F3FE}\", \"\\u{1F3FF}\"];\nconst TONES = [0, 1, 2, 3, 4, 5] as const;\n\nfunction ToneGlyph({ tone }: { tone: EmojiPickerSkinTone }) {\n  return (\n    <span aria-hidden=\"true\" className={cn(EMOJI_FONT_CLASS, \"text-base\")}>\n      {HAND + TONE_MODIFIERS[tone]}\n    </span>\n  );\n}\n\ntype EmojiPickerSkinToneProps = Omit<ComponentProps<typeof Button>, \"children\">;\n\n/**\n * A Button with a hand in the current tone that opens the six tones in a\n * DropdownMenu. The choice applies to every emoji that takes a tone and is\n * remembered under `skinToneStorageKey`. Ghost at `icon-sm` for the footer by\n * default; beside the search, `outline` at `icon` matches its height.\n */\nfunction EmojiPickerSkinTone({\n  variant = \"ghost\",\n  size = \"icon-sm\",\n  ...props\n}: EmojiPickerSkinToneProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerSkinTone\");\n  const { labels } = context.i18n;\n  const tone = context.skinTone;\n\n  return (\n    <DropdownMenuTrigger>\n      <Button\n        variant={variant}\n        size={size}\n        isDisabled={context.disabled}\n        aria-label={`${labels.skinTone}: ${labels.skinTones[tone]}`}\n        data-slot=\"emoji-picker-skin-tone\"\n        {...props}\n      >\n        <ToneGlyph tone={tone} />\n      </Button>\n      <DropdownMenu\n        placement=\"bottom end\"\n        className=\"min-w-40\"\n        selectionMode=\"single\"\n        disallowEmptySelection\n        selectedKeys={[String(tone)]}\n        onSelectionChange={(keys) => {\n          const [key] = keys === \"all\" ? [] : [...keys];\n          if (key !== undefined) context.setSkinTone(Number(key) as EmojiPickerSkinTone);\n        }}\n      >\n        <DropdownMenuGroup>\n          <DropdownMenuLabel>{labels.skinTone}</DropdownMenuLabel>\n          {TONES.map((option) => (\n            <DropdownMenuItem key={option} id={String(option)} textValue={labels.skinTones[option]}>\n              <ToneGlyph tone={option} />\n              {labels.skinTones[option]}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenu>\n    </DropdownMenuTrigger>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                 Categories                                 */\n/* -------------------------------------------------------------------------- */\n\n/* The triggers share the row evenly, so any number of categories fits; a\n   trigger's own padding would overflow a row of 11. */\nconst TAB_CLASS = \"max-w-9 min-w-0 px-0\";\n\ntype EmojiPickerCategoriesProps = ComponentProps<\"div\">;\n\n/**\n * Category tabs, the Tabs in its line variant, that jump the grid and\n * follow its scroll. Arrow keys move between them; Enter or Space jumps. A\n * jump during a search clears it.\n */\nfunction EmojiPickerCategories({ className, ...props }: EmojiPickerCategoriesProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerCategories\");\n  const searching = !!context.query.trim();\n  const placeholder = context.status !== \"ready\";\n  const tabs = placeholder ? context.placeholderTabs : context.tabs;\n  const current = searching ? \"\" : (context.activeSection ?? tabs[0]?.id ?? \"\");\n\n  return (\n    <div\n      data-slot=\"emoji-picker-categories\"\n      className={cn(\n        /* A divider on the grid's side: below the tabs over the grid, above\n           them under it. */\n        \"shrink-0 border-b px-(--emoji-picker-padding) first:pt-(--emoji-picker-padding) last:pb-(--emoji-picker-padding) [[data-slot=emoji-picker-list]~&]:border-t [[data-slot=emoji-picker-list]~&]:border-b-0 [[data-slot=emoji-picker-list]~&]:pt-1\",\n        className,\n      )}\n      {...props}\n    >\n      {/* Controlled by the scroll, not by a pick: a press jumps, and the\n          grid's scroll then moves the tab. Manual activation, so the arrows\n          move between tabs and Enter or Space jumps. An unmatched key (during\n          a search) selects no tab. */}\n      <Tabs\n        selectedKey={current}\n        keyboardActivation=\"manual\"\n        onSelectionChange={(key) => context.jumpTo(String(key))}\n      >\n        <TabsList\n          variant=\"line\"\n          aria-label={context.i18n.labels.categories}\n          className=\"w-full justify-between\"\n        >\n          {tabs.map((tab) => (\n            <TabsTrigger\n              key={tab.id}\n              id={tab.id}\n              aria-label={tab.label}\n              isDisabled={placeholder || context.disabled}\n              data-slot=\"emoji-picker-category\"\n              data-category={tab.id}\n              className={TAB_CLASS}\n            >\n              {tab.icon}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n      </Tabs>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    List                                    */\n/* -------------------------------------------------------------------------- */\n\nconst optionId = (listId: string, at: { row: number; col: number }) =>\n  `${listId}-${at.row}-${at.col}`;\n\n/* The shadcn Command group heading per style, at a fixed height so the virtual\n   grid can place rows without measuring. The text starts where the glyphs\n   below it do. `pt-1` spends the header's last 4px above the label, so it\n   sits with its emoji: at the default size 11px above its ink, 10.5px below\n   (the old centered 24px header left 7px above). */\nconst SECTION_HEADER_CLASS =\n  \"sticky top-0 z-1 flex h-(--emoji-picker-header) items-center bg-(--emoji-picker-surface) px-(--emoji-picker-padding) pt-1 text-xs text-muted-foreground select-none font-medium\";\n\n/* The shadcn Button radius per style: a cell is a ghost icon button, and a\n   highlight paints its background only; the glyph never changes size. */\nconst CELL_CLASS =\n  \"mx-auto flex size-(--emoji-picker-cell) cursor-default items-center justify-center text-(length:--emoji-picker-emoji) transition-colors duration-100 select-none data-active:bg-accent aria-disabled:opacity-40 data-selected:bg-primary/10 data-selected:inset-ring-1 data-selected:inset-ring-primary/30 dark:data-selected:bg-primary/20 forced-colors:data-active:outline-2 forced-colors:data-active:outline-solid rounded-lg\";\n\n/* The keyboard's cell takes the shadcn Button's focus in each style, border\n   and ring, the ring no wider than the gutter so the grid's edge never cuts\n   it. It rises over the sticky header, whose empty bottom it may cross. */\nconst CELL_FOCUS_CLASS =\n  \"data-keyboard:z-2 data-keyboard:border data-keyboard:border-ring data-keyboard:ring-[length:min(3px,var(--emoji-picker-gutter))] data-keyboard:ring-ring/50\";\n\n/* No scrollbar; a fade at each end that has rows scrolled out of view says\n   there is more. The top fade starts under the sticky header, which stays\n   solid. `--emoji-picker-fade-size` sets the depth, `0px` turns it off, as\n   the keyboard does: a mask also fades the focus ring it would sit on. */\nconst LIST_CLASS =\n  \"relative h-(--emoji-picker-list-height) min-h-[calc(var(--emoji-picker-header)+var(--emoji-picker-cell)*2)] overflow-y-auto overscroll-contain outline-none [scrollbar-width:none] data-keyboard:[--emoji-picker-fade-size:0px] [&::-webkit-scrollbar]:hidden [mask-image:linear-gradient(to_bottom,#000_var(--emoji-picker-header),transparent_var(--emoji-picker-header),#000_calc(var(--emoji-picker-header)+var(--emoji-picker-fade-start,0px)),#000_calc(100%-var(--emoji-picker-fade-end,0px)),transparent)] data-overflow-start:[--emoji-picker-fade-start:var(--emoji-picker-fade-size,calc(var(--emoji-picker-cell)*2/3))] data-overflow-end:[--emoji-picker-fade-end:var(--emoji-picker-fade-size,calc(var(--emoji-picker-cell)*2/3))]\";\n\n/* Scrolls the grid and notes where, so a scroll of its own is told from the\n   user's. */\nfunction scrollGrid(list: HTMLElement, top: number, mark: RefObject<number | null>) {\n  list.scrollTop = top;\n  mark.current = list.scrollTop;\n}\n\ntype EmojiPickerListProps = ComponentProps<\"div\">;\n\n/**\n * The virtual grid: only the rows in view render, so a page of reaction\n * pickers or a 20,000 custom emoji library stays cheap. Section headers stick\n * while their rows scroll. Holds the loading, error and empty states.\n */\nfunction EmojiPickerList({\n  className,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  onPointerDown,\n  onScroll,\n  ...props\n}: EmojiPickerListProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerList\");\n  const {\n    layout,\n    size,\n    requestData,\n    setActiveSection,\n    setMeasuredColumns,\n    setJump,\n    listRef,\n    jump,\n  } = context;\n  const { labels, functions } = context.i18n;\n  const [range, setRange] = useState({ start: 0, end: VISIBLE_ROWS + OVERSCAN });\n  const updateRef = useRef(() => {});\n  /* A click on the grid's own background focuses it without a highlight. */\n  const pointerDown = useRef(false);\n  /* Where the grid last scrolled itself; any other scroll is the user's. */\n  const scrolledTo = useRef<number | null>(null);\n\n  const update = () => {\n    const list = listRef.current;\n    if (!list) return;\n    const height = list.clientHeight || FALLBACK_HEIGHT;\n    const top = list.scrollTop;\n    const last = Math.max(0, layout.rows.length - 1);\n    const start = Math.max(0, indexAt(layout.rows, top) - OVERSCAN);\n    const end = Math.min(last, indexAt(layout.rows, top + height) + OVERSCAN);\n    setRange((previous) =>\n      previous.start === start && previous.end === end ? previous : { start, end },\n    );\n    context.setPageRows(Math.max(1, Math.floor((height - size.header) / size.cell)));\n    list.toggleAttribute(\"data-overflow-start\", top > 1);\n    list.toggleAttribute(\"data-overflow-end\", top + list.clientHeight < list.scrollHeight - 1);\n    setActiveSection(layout.sections[indexAt(layout.sections, top + 1)]?.section.id ?? null);\n  };\n\n  useLayoutEffect(() => {\n    updateRef.current = update;\n  });\n\n  useEffect(() => {\n    requestData();\n  }, [requestData]);\n\n  /* A new query starts at the top. */\n  // biome-ignore lint/correctness/useExhaustiveDependencies: runs on each new query\n  useLayoutEffect(() => {\n    if (listRef.current) scrollGrid(listRef.current, 0, scrolledTo);\n    updateRef.current();\n  }, [context.query, listRef]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-reads the window for each new layout\n  useLayoutEffect(() => {\n    updateRef.current();\n  }, [layout]);\n\n  useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!jump || !list) return;\n    const section = layout.sections.find((item) => item.section.id === jump);\n    if (!section) return;\n    scrollGrid(list, section.top, scrolledTo);\n    updateRef.current();\n    setJump(null);\n  }, [jump, layout, listRef, setJump]);\n\n  /* Keeps a keyboard-highlighted cell clear of the sticky header and the\n     bottom edge. A hovered one is already in view. */\n  const { active, activePosition } = context;\n  useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!list || active?.source !== \"keyboard\" || !activePosition) return;\n    const row = layout.rows[activePosition.row];\n    if (!row) return;\n    const height = list.clientHeight || FALLBACK_HEIGHT;\n    if (row.top - size.header < list.scrollTop) {\n      scrollGrid(list, row.top - size.header, scrolledTo);\n    } else if (row.top + size.cell + size.gutter > list.scrollTop + height) {\n      scrollGrid(list, row.top + size.cell + size.gutter - height, scrolledTo);\n    }\n    updateRef.current();\n  }, [active, activePosition, layout, listRef, size]);\n\n  /* Re-reads the window and the fades on resize and, under\n     `columns=\"auto\"`, how many cells fit. */\n  const { columns } = context;\n  useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!list) return;\n    const measure = () => {\n      if (columns === \"auto\" && list.clientWidth > 0) {\n        setMeasuredColumns(\n          Math.max(1, Math.floor((list.clientWidth - size.gutter * 2) / size.cell)),\n        );\n      }\n      updateRef.current();\n    };\n    measure();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    return () => observer.disconnect();\n  }, [columns, listRef, setMeasuredColumns, size]);\n\n  const rendered = new Set<number>();\n  for (let row = range.start; row <= range.end && row < layout.rows.length; row++) {\n    rendered.add(row);\n  }\n  /* The highlighted row always renders, so aria-activedescendant resolves. */\n  if (activePosition) rendered.add(activePosition.row);\n  const rowsBySection = new Map<number, number[]>();\n  for (const row of [...rendered].sort((a, b) => a - b)) {\n    const section = layout.rows[row]?.section;\n    if (section === undefined) continue;\n    const list = rowsBySection.get(section);\n    if (list) list.push(row);\n    else rowsBySection.set(section, [row]);\n  }\n\n  const searching = !!context.query.trim();\n  const ready = context.status === \"ready\";\n  const empty = ready && layout.rows.length === 0;\n  /* The keyboard's highlight shows only while the search or the grid holds\n     focus, and pauses the fades. */\n  const keyboard = context.focused && context.active?.source === \"keyboard\" && !!activePosition;\n  const status =\n    searching && ready\n      ? functions.formatResultCount(layout.rows.reduce((sum, row) => sum + row.cells.length, 0))\n      : \"\";\n\n  return (\n    <>\n      <div\n        ref={listRef}\n        id={context.listId}\n        role=\"listbox\"\n        aria-label={labels.list}\n        aria-multiselectable={context.multiple || undefined}\n        aria-busy={!ready || undefined}\n        aria-activedescendant={\n          activePosition ? optionId(context.listId, activePosition) : undefined\n        }\n        /* A tab stop of its own, search or not, once there is an emoji. */\n        tabIndex={context.disabled || !ready || empty ? -1 : 0}\n        data-slot=\"emoji-picker-list\"\n        data-keyboard={keyboard ? \"\" : undefined}\n        className={cn(LIST_CLASS, className)}\n        onScroll={(event) => {\n          onScroll?.(event);\n          const mark = scrolledTo.current;\n          if (mark === null || Math.abs(event.currentTarget.scrollTop - mark) > 1)\n            context.releaseKeyboard();\n          update();\n        }}\n        onPointerDown={(event) => {\n          onPointerDown?.(event);\n          if (!event.defaultPrevented) pointerDown.current = true;\n        }}\n        onFocus={(event) => {\n          onFocus?.(event);\n          context.setFocused(true);\n          if (event.target !== event.currentTarget) return;\n          const fromPointer = pointerDown.current || isTouch();\n          pointerDown.current = false;\n          /* Tabbing in starts at the first emoji in view; a highlight the\n             keyboard already holds stays where it is. */\n          if (!fromPointer && context.active?.source !== \"keyboard\") {\n            context.move(\"visible\");\n          }\n        }}\n        onBlur={(event) => {\n          onBlur?.(event);\n          pointerDown.current = false;\n          context.setFocused(false);\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented || event.target !== event.currentTarget) return;\n          /* Typing in the grid goes on in the search. */\n          const search = context.searchRef.current;\n          const typed = (event.key.length === 1 && event.key !== \" \") || event.key === \"Backspace\";\n          if (\n            search &&\n            typed &&\n            !event.nativeEvent.isComposing &&\n            !event.altKey &&\n            !event.ctrlKey &&\n            !event.metaKey\n          ) {\n            search.focus();\n            return;\n          }\n          context.navigate(event, false);\n        }}\n        {...props}\n      >\n        {ready && (\n          <div className=\"relative\" style={{ height: layout.height }}>\n            {layout.sections.map((section, sectionIndex) => {\n              const rows = rowsBySection.get(sectionIndex);\n              if (!rows) return null;\n              const headerId = `${context.listId}-${sectionIndex}`;\n              return (\n                <div\n                  key={section.section.id}\n                  role=\"group\"\n                  aria-labelledby={headerId}\n                  data-slot=\"emoji-picker-section\"\n                  data-section={section.section.id}\n                  className=\"absolute inset-x-0\"\n                  style={{ top: section.top, height: section.height }}\n                >\n                  <div\n                    id={headerId}\n                    data-slot=\"emoji-picker-section-header\"\n                    className={SECTION_HEADER_CLASS}\n                  >\n                    {section.section.label}\n                  </div>\n                  {rows.map((rowIndex) => {\n                    const row = layout.rows[rowIndex];\n                    const first = (rowIndex - section.firstRow) * context.columnCount;\n                    return (\n                      <div\n                        key={rowIndex}\n                        role=\"presentation\"\n                        className=\"absolute inset-x-(--emoji-picker-gutter) grid grid-cols-[repeat(var(--emoji-picker-columns),minmax(0,1fr))]\"\n                        style={{ top: row.top - section.top }}\n                      >\n                        {row.cells.map((emoji, col) => {\n                          const at = { row: rowIndex, col };\n                          const isActive =\n                            activePosition?.row === rowIndex && activePosition.col === col;\n                          const isSelected = context.isSelected(emoji);\n                          const refused = context.full && !isSelected;\n                          const value = getEmojiValue(emoji, emoji.skins ? context.skinTone : 0);\n                          return (\n                            <div\n                              key={emoji.id}\n                              id={optionId(context.listId, at)}\n                              role=\"option\"\n                              aria-label={functions.formatLabel(emoji.label)}\n                              aria-selected={context.valueMode ? isSelected : isActive}\n                              aria-disabled={refused || undefined}\n                              aria-posinset={first + col + 1}\n                              aria-setsize={section.section.emojis.length}\n                              data-slot=\"emoji-picker-emoji\"\n                              data-active={isActive ? \"\" : undefined}\n                              data-keyboard={keyboard && isActive ? \"\" : undefined}\n                              /* \"true\", not \"\": shadcn's tailwind.css matches\n                                 `data-selected:` on that value only. */\n                              data-selected={isSelected && context.valueMode ? \"true\" : undefined}\n                              className={cn(CELL_CLASS, CELL_FOCUS_CLASS)}\n                              /* Keeps focus in the search, so typing goes on. A\n                                 panel that does not hold focus yet takes it\n                                 there, or on the list on a touch screen, where\n                                 the search would raise the keyboard. */\n                              onPointerDown={(event) => {\n                                event.preventDefault();\n                                const { searchRef, listRef } = context;\n                                const focused = event.currentTarget.ownerDocument.activeElement;\n                                if (focused === searchRef.current || focused === listRef.current)\n                                  return;\n                                const target =\n                                  event.pointerType === \"touch\"\n                                    ? listRef.current\n                                    : (searchRef.current ?? listRef.current);\n                                if (target === listRef.current) {\n                                  pointerDown.current = true;\n                                }\n                                target?.focus({ preventScroll: true });\n                              }}\n                              onPointerMove={() => {\n                                if (isActive && context.active?.source === \"pointer\") return;\n                                context.setActive({\n                                  section: section.section.id,\n                                  id: emoji.id,\n                                  source: \"pointer\",\n                                });\n                              }}\n                              onClick={(event) => {\n                                if (!refused) context.pick(emoji, event);\n                              }}\n                            >\n                              <EmojiGlyph emoji={emoji} value={value} />\n                            </div>\n                          );\n                        })}\n                      </div>\n                    );\n                  })}\n                </div>\n              );\n            })}\n          </div>\n        )}\n        {!ready && context.status !== \"error\" && (\n          <div role=\"status\" aria-label={labels.loading} className=\"flex flex-col\">\n            <div className=\"flex h-(--emoji-picker-header) items-center px-(--emoji-picker-padding) pt-1\">\n              <Skeleton className=\"h-3 w-24\" />\n            </div>\n            <div className=\"grid grid-cols-[repeat(var(--emoji-picker-columns),minmax(0,1fr))] px-(--emoji-picker-gutter)\">\n              {Array.from({ length: context.columnCount * 5 }, (_, index) => `cell-${index}`).map(\n                (key) => (\n                  <div\n                    key={key}\n                    className=\"flex size-(--emoji-picker-cell) items-center justify-center\"\n                  >\n                    <Skeleton className=\"size-(--emoji-picker-emoji) rounded-full\" />\n                  </div>\n                ),\n              )}\n            </div>\n          </div>\n        )}\n        {context.status === \"error\" && (\n          <Empty className=\"h-full\">\n            <EmptyHeader>\n              <EmptyTitle>{labels.error}</EmptyTitle>\n            </EmptyHeader>\n            <EmptyContent>\n              <Button variant=\"outline\" size=\"sm\" onPress={context.retry}>\n                {labels.retry}\n              </Button>\n            </EmptyContent>\n          </Empty>\n        )}\n        {empty && (\n          <Empty data-slot=\"emoji-picker-empty\" className=\"h-full\">\n            <EmptyHeader>\n              <EmptyMedia variant=\"icon\">{SEARCH_ICON}</EmptyMedia>\n              <EmptyTitle>{labels.empty}</EmptyTitle>\n              <EmptyDescription>{labels.emptyHint}</EmptyDescription>\n            </EmptyHeader>\n          </Empty>\n        )}\n      </div>\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {context.announcement || status}\n      </div>\n    </>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Preview                                  */\n/* -------------------------------------------------------------------------- */\n\ntype EmojiPickerFooterProps = ComponentProps<\"div\">;\n\n/** The row under the grid: the preview and the skin tone by default. */\nfunction EmojiPickerFooter({ className, ...props }: EmojiPickerFooterProps) {\n  return (\n    <div\n      data-slot=\"emoji-picker-footer\"\n      className={cn(\n        \"flex h-10 shrink-0 items-center gap-2 border-t px-(--emoji-picker-padding)\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype EmojiPickerPreviewProps = ComponentProps<\"div\">;\n\n/**\n * The highlighted emoji, large, with its name and first shortcode; the value\n * while nothing is highlighted. Visual only: the grid already announces it.\n * It fills the space it is given, usually an `EmojiPickerFooter`.\n */\nfunction EmojiPickerPreview({ className, ...props }: EmojiPickerPreviewProps) {\n  const context = useEmojiPickerContext(\"EmojiPickerPreview\");\n  const { labels, functions } = context.i18n;\n  const fallback =\n    !context.multiple && context.values[0] ? context.findByValue(context.values[0]) : undefined;\n  const emoji = context.activeEmoji ?? fallback ?? null;\n  const value = emoji\n    ? context.activeEmoji\n      ? getEmojiValue(emoji, emoji.skins ? context.skinTone : 0)\n      : (context.values[0] as string)\n    : \"\";\n  const shortcode = emoji?.shortcodes[0];\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-slot=\"emoji-picker-preview\"\n      className={cn(\"flex min-w-0 flex-1 items-center gap-2 select-none\", className)}\n      {...props}\n    >\n      {emoji ? (\n        <>\n          <EmojiGlyph emoji={emoji} value={value} className=\"shrink-0 text-[22px]\" />\n          {/* One line, so the footer stays as dense as the grid. Without room\n              the shortcode wraps onto a second, clipped line rather than\n              truncating to a stray colon; a long name truncates. */}\n          <span className=\"flex h-5 min-w-0 flex-1 flex-wrap items-baseline gap-x-2 overflow-hidden\">\n            <span className=\"max-w-full truncate text-sm leading-5 font-medium\">\n              {functions.formatLabel(emoji.label)}\n            </span>\n            {shortcode && (\n              <span className=\"text-muted-foreground text-xs leading-5\">:{shortcode}:</span>\n            )}\n          </span>\n        </>\n      ) : (\n        <>\n          <span className=\"text-muted-foreground flex size-[22px] shrink-0 items-center justify-center [&_svg]:size-5\">\n            {TRIGGER_ICON}\n          </span>\n          <span className=\"text-muted-foreground truncate text-sm\">{labels.preview}</span>\n        </>\n      )}\n    </div>\n  );\n}\n\nexport type {\n  EmojiPickerApi,\n  EmojiPickerCategoriesProps,\n  EmojiPickerCategory,\n  EmojiPickerContentProps,\n  EmojiPickerCustomCategory,\n  EmojiPickerCustomEmoji,\n  EmojiPickerData,\n  EmojiPickerDataOptions,\n  EmojiPickerDataState,\n  EmojiPickerDataStatus,\n  EmojiPickerEmoji,\n  EmojiPickerFooterProps,\n  EmojiPickerFunctions,\n  EmojiPickerHeaderProps,\n  EmojiPickerI18nConfig,\n  EmojiPickerI18nOverrides,\n  EmojiPickerLabels,\n  EmojiPickerListProps,\n  EmojiPickerPanelProps,\n  EmojiPickerPreviewProps,\n  EmojiPickerProps,\n  EmojiPickerRecentEntry,\n  EmojiPickerSearchProps,\n  EmojiPickerSelection,\n  EmojiPickerShortcodePreset,\n  EmojiPickerSize,\n  EmojiPickerSkinToneProps,\n  EmojiPickerTriggerProps,\n  EmojiPickerValueProps,\n  UseEmojiPickerDataOptions,\n};\nexport {\n  DEFAULT_EMOJI_PICKER_I18N,\n  EMOJI_PICKER_CATEGORIES,\n  EMOJI_PICKER_DATA_URL,\n  EmojiPicker,\n  EmojiPickerCategories,\n  EmojiPickerContent,\n  EmojiPickerFooter,\n  EmojiPickerHeader,\n  EmojiPickerList,\n  EmojiPickerPanel,\n  EmojiPickerPreview,\n  EmojiPickerSearch,\n  EmojiPickerSkinTone,\n  EmojiPickerTrigger,\n  EmojiPickerValue,\n  findEmoji,\n  getEmojiValue,\n  isEmojiValue,\n  loadEmojiPickerData,\n  mergeEmojiPickerI18n,\n  normalizeEmojibaseData,\n  searchEmojis,\n  useEmojiPicker,\n  useEmojiPickerData,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/emoji-picker.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "third-party",
    "license": "MIT",
    "origin": "reui.io (https://reui.io/docs/components/base/emoji-picker, MIT); rebuilt on react-aria Popover, Dialog, Button, Tabs and Menu",
    "tags": [
      "forms",
      "emoji",
      "picker",
      "popover",
      "react-aria"
    ],
    "contentHash": "401d10890e88"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}