{"version":3,"file":"index.mjs","sources":["../../../src/primitives/EmojiPicker/index.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport type { ChangeEvent, KeyboardEvent, SyntheticEvent } from \"react\";\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type {\n  GroupedVirtuosoHandle,\n  ListProps as VirtuosoListProps,\n  ScrollerProps,\n  TopItemListProps,\n} from \"react-virtuoso\";\nimport { GroupedVirtuoso } from \"react-virtuoso\";\n\nimport { isKey } from \"../../utils/is-key\";\nimport {\n  cancelIdleCallback,\n  requestIdleCallback,\n} from \"../../utils/request-idle-callback\";\nimport { useTransition } from \"../../utils/use-transition\";\nimport { visuallyHidden } from \"../../utils/visually-hidden\";\nimport { Emoji as EmojiPrimitive } from \"../internal/Emoji\";\nimport { EmojiPickerContext, useEmojiPicker } from \"./contexts\";\nimport type {\n  EmojiData,\n  EmojiPickerContentComponents,\n  EmojiPickerContentEmojiRowAttributes,\n  EmojiPickerContentProps,\n  EmojiPickerData,\n  EmojiPickerRootProps,\n  EmojiPickerSearchProps,\n  EmojiPickerSelectionDirection,\n} from \"./types\";\nimport { filterEmojis, generateEmojiPickerData, getEmojiData } from \"./utils\";\n\nconst DEFAULT_COLUMNS = 10;\nconst DEFAULT_LOCALE = \"en\";\nconst LOADING_MINIMUM_TIMEOUT = 100;\n\nconst EMOJIPICKER_ROOT_NAME = \"EmojiPickerRoot\";\nconst EMOJIPICKER_CONTENT_NAME = \"EmojiPickerContent\";\nconst EMOJIPICKER_SEARCH_NAME = \"EmojiPickerSearch\";\n\n/**\n * @private\n * The EmojiPicker primitive is undocumented for now and subject to change,\n * use at your own risk. If you have any feedback on it, please let us know!\n * See how we use it in the default components to learn how to use it:\n * https://github.com/liveblocks/liveblocks/blob/main/packages/liveblocks-react-comments/src/components/internal/EmojiPicker.tsx.\n *\n * Surrounds the emoji picker, it handles emoji data and coordinates\n * `EmojiPicker.Search` and `EmojiPicker.Content`.\n *\n * @example\n * <EmojiPicker.Root>\n *   <EmojiPicker.Search />\n *   <EmojiPicker.Content />\n * </EmojiPicker.Root>\n */\nfunction EmojiPickerRoot({\n  columns = DEFAULT_COLUMNS,\n  locale = DEFAULT_LOCALE,\n  onEmojiSelect,\n  children,\n}: EmojiPickerRootProps) {\n  const emojiData = useRef<EmojiData>();\n  const search = useRef(\"\");\n  const [, startEmojisTransition] = useTransition();\n  const [data, setData] = useState<EmojiPickerData>();\n  const [error, setError] = useState<Error>();\n  const [selectedColumnIndex, setSelectedColumnIndex] = useState(0);\n  const [selectedRowIndex, setSelectedRowIndex] = useState(0);\n  const [interaction, setInteraction] = useState<\n    \"keyboard\" | \"pointer\" | \"none\"\n  >(\"none\");\n\n  const selectCurrentEmoji = useCallback(() => {\n    if (onEmojiSelect) {\n      const emoji = data?.rows[selectedRowIndex]?.[selectedColumnIndex];\n\n      if (emoji) {\n        onEmojiSelect(emoji.emoji);\n      }\n    }\n  }, [data?.rows, onEmojiSelect, selectedColumnIndex, selectedRowIndex]);\n\n  const resetSelection = useCallback(() => {\n    setSelectedColumnIndex(0);\n    setSelectedRowIndex(0);\n  }, []);\n\n  const setPointerSelection = useCallback(\n    (columnIndex: number, rowIndex: number) => {\n      setInteraction(\"pointer\");\n      setSelectedColumnIndex(columnIndex);\n      setSelectedRowIndex(rowIndex);\n    },\n    []\n  );\n\n  const moveSelection = useCallback(\n    (\n      direction: EmojiPickerSelectionDirection,\n      event: KeyboardEvent<HTMLInputElement>\n    ) => {\n      if (!data) {\n        return;\n      }\n\n      event.preventDefault();\n\n      if (interaction === \"none\") {\n        setInteraction(\"keyboard\");\n        return;\n      }\n\n      setInteraction(\"keyboard\");\n\n      switch (direction) {\n        // If first column, move to last column of previous row (if available)\n        // Otherwise, move to previous column\n        case \"left\": {\n          if (selectedColumnIndex === 0) {\n            const previousRowIndex = selectedRowIndex - 1;\n            const previousRow = data.rows[previousRowIndex];\n\n            if (previousRow) {\n              setSelectedRowIndex(previousRowIndex);\n              setSelectedColumnIndex(previousRow.length - 1);\n            }\n          } else {\n            setSelectedColumnIndex(selectedColumnIndex - 1);\n          }\n\n          break;\n        }\n\n        // If last column, move to first column of next row (if available)\n        // Otherwise, move to next column\n        case \"right\": {\n          const currentRow = data.rows[selectedRowIndex];\n\n          if (selectedColumnIndex === currentRow.length - 1) {\n            const nextRowIndex = selectedRowIndex + 1;\n            const nextRow = data.rows[nextRowIndex];\n\n            if (nextRow) {\n              setSelectedRowIndex(nextRowIndex);\n              setSelectedColumnIndex(0);\n            }\n          } else {\n            setSelectedColumnIndex(selectedColumnIndex + 1);\n          }\n\n          break;\n        }\n\n        // Move to same column of previous row\n        // If same column is not available, move to last column of previous row\n        case \"up\": {\n          const previousRow = data.rows[selectedRowIndex - 1];\n\n          if (previousRow) {\n            setSelectedRowIndex(selectedRowIndex - 1);\n\n            if (!previousRow[selectedColumnIndex]) {\n              setSelectedColumnIndex(previousRow.length - 1);\n            }\n          }\n\n          break;\n        }\n\n        // Move to same column of next row\n        // If same column is not available, move to last column of next row\n        case \"down\": {\n          const nextRow = data.rows[selectedRowIndex + 1];\n\n          if (nextRow) {\n            setSelectedRowIndex(selectedRowIndex + 1);\n\n            if (!nextRow[selectedColumnIndex]) {\n              setSelectedColumnIndex(nextRow.length - 1);\n            }\n          }\n\n          break;\n        }\n      }\n    },\n    [data, interaction, selectedColumnIndex, selectedRowIndex]\n  );\n\n  const updateEmojis = useCallback(() => {\n    if (!emojiData.current) {\n      return;\n    }\n\n    startEmojisTransition(() => {\n      setData(() => {\n        if (!emojiData.current) {\n          return;\n        }\n\n        const filteredEmojis = filterEmojis(\n          emojiData.current.emojis,\n          search.current\n        );\n\n        return generateEmojiPickerData(\n          filteredEmojis,\n          emojiData.current.categories,\n          columns\n        );\n      });\n      resetSelection();\n    });\n  }, [columns, resetSelection]);\n\n  const handleSearch = useCallback(\n    (value: string) => {\n      search.current = value;\n      updateEmojis();\n    },\n    [updateEmojis]\n  );\n\n  const initializeEmojiData = useCallback(\n    async (locale: string) => {\n      try {\n        emojiData.current = await getEmojiData(locale);\n        updateEmojis();\n      } catch (error) {\n        setError(error as Error);\n      }\n    },\n    [updateEmojis]\n  );\n\n  useEffect(() => {\n    let idleCallbackId: number;\n    const timeoutId = setTimeout(() => {\n      idleCallbackId = requestIdleCallback(() => {\n        initializeEmojiData(locale);\n      });\n    }, LOADING_MINIMUM_TIMEOUT);\n\n    return () => {\n      clearTimeout(timeoutId);\n      cancelIdleCallback(idleCallbackId);\n    };\n  }, [locale]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  useEffect(() => {\n    if (interaction === \"none\") {\n      resetSelection();\n    }\n  }, [interaction]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  return (\n    <EmojiPickerContext.Provider\n      value={{\n        data: data as EmojiPickerData,\n        error: error as undefined,\n        isLoading: (!data && !error) as false,\n        columns,\n        onSearch: handleSearch,\n        onEmojiSelect,\n        selectCurrentEmoji,\n        selectedRowIndex,\n        selectedColumnIndex,\n        moveSelection,\n        setPointerSelection,\n        interaction,\n        setInteraction,\n      }}\n    >\n      {children}\n    </EmojiPickerContext.Provider>\n  );\n}\n\n/**\n * @private\n * The EmojiPicker primitive is undocumented for now and subject to change,\n * use at your own risk. If you have any feedback on it, please let us know!\n * See how we use it in the default components to learn how to use it:\n * https://github.com/liveblocks/liveblocks/blob/main/packages/liveblocks-react-comments/src/components/internal/EmojiPicker.tsx.\n *\n * The search input of the emoji picker. It also affects the focus and selection\n * within `EmojiPicker.Content`.\n *\n * @example\n * <EmojiPicker.Search />\n */\nconst EmojiPickerSearch = forwardRef<HTMLInputElement, EmojiPickerSearchProps>(\n  ({ asChild, value, defaultValue, onChange, ...props }, forwardedRef) => {\n    const Component = asChild ? Slot : \"input\";\n    const {\n      onSearch,\n      selectCurrentEmoji,\n      moveSelection,\n      interaction,\n      setInteraction,\n    } = useEmojiPicker();\n\n    const handleChange = useCallback(\n      (event: ChangeEvent<HTMLInputElement>) => {\n        onChange?.(event);\n\n        if (event.isDefaultPrevented()) {\n          return;\n        }\n\n        const value = event.target.value;\n        setInteraction(value ? \"keyboard\" : \"none\");\n        onSearch(value);\n      },\n      [onChange, onSearch, setInteraction]\n    );\n\n    const handleKeyDown = useCallback(\n      (event: KeyboardEvent<HTMLInputElement>) => {\n        if (event.isDefaultPrevented()) {\n          return;\n        }\n\n        if (isKey(event, \"ArrowLeft\")) {\n          moveSelection(\"left\", event);\n        } else if (isKey(event, \"ArrowRight\")) {\n          moveSelection(\"right\", event);\n        } else if (isKey(event, \"ArrowUp\")) {\n          moveSelection(\"up\", event);\n        } else if (isKey(event, \"ArrowDown\")) {\n          moveSelection(\"down\", event);\n        } else if (isKey(event, \"Enter\")) {\n          if (interaction !== \"none\") {\n            event.preventDefault();\n            selectCurrentEmoji();\n          }\n        }\n      },\n      [interaction, moveSelection, selectCurrentEmoji]\n    );\n\n    useEffect(() => {\n      onSearch(\n        value ? String(value) : defaultValue ? String(defaultValue) : \"\"\n      );\n    }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n    return (\n      <Component\n        type=\"search\"\n        value={value}\n        defaultValue={defaultValue}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        {...props}\n        ref={forwardedRef}\n      />\n    );\n  }\n);\n\nconst defaultContentComponents: EmojiPickerContentComponents = {\n  CategoryHeader: ({ category, ...props }) => <div {...props}>{category}</div>,\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  Row: ({ children, attributes, ...props }) => <div {...props}>{children}</div>,\n  Emoji: ({ emoji, ...props }) => (\n    <button {...props}>\n      <EmojiPrimitive emoji={emoji} />\n    </button>\n  ),\n  Loading: (props) => <div {...props} />,\n  Empty: (props) => <div {...props} />,\n  Grid: (props) => <div {...props} />,\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  Error: ({ error, ...props }) => <div {...props} />,\n};\n\nconst placeholderRowAttributes: EmojiPickerContentEmojiRowAttributes = {\n  rowIndex: -1,\n  categoryRowIndex: -1,\n  categoryRowsCount: 0,\n};\n\nconst VirtuosoScroller = forwardRef<HTMLDivElement, ScrollerProps>(\n  ({ children, ...props }, forwardedRef) => {\n    return (\n      <div {...props} tabIndex={-1} data-test-id={undefined} ref={forwardedRef}>\n        {children}\n      </div>\n    );\n  }\n);\n\nconst VirtuosoTopList = forwardRef<HTMLDivElement, TopItemListProps>(\n  ({ children, ...props }, forwardedRef) => {\n    return (\n      <div {...props} data-test-id={undefined} ref={forwardedRef}>\n        {children}\n      </div>\n    );\n  }\n);\n\n/**\n * @private\n * The EmojiPicker primitive is undocumented for now and subject to change,\n * use at your own risk. If you have any feedback on it, please let us know!\n * See how we use it in the default components to learn how to use it:\n * https://github.com/liveblocks/liveblocks/blob/main/packages/liveblocks-react-comments/src/components/internal/EmojiPicker.tsx.\n *\n * The main content of the emoji picker, either displaying the emoji grid or various\n * alternative states (loading, empty, and error).\n *\n * @example\n * <EmojiPicker.Content\n *  components={{\n *    Loading: EmojiPickerLoading,\n *    Empty: EmojiPickerEmpty,\n *    Error: EmojiPickerError,\n *    CategoryHeader: EmojiPickerCategoryHeader,\n *    Grid: EmojiPickerGrid,\n *    Row: EmojiPickerRow,\n *    Emoji: EmojiPickerEmoji,\n *  }}\n * />\n */\nconst EmojiPickerContent = forwardRef<HTMLDivElement, EmojiPickerContentProps>(\n  ({ components, asChild, ...props }, forwardedRef) => {\n    const Component = asChild ? Slot : \"div\";\n    const virtuosoRef = useRef<GroupedVirtuosoHandle>(null);\n    const {\n      data,\n      error,\n      isLoading,\n      columns,\n      onEmojiSelect,\n      selectedColumnIndex,\n      selectedRowIndex,\n      setPointerSelection,\n      interaction,\n      setInteraction,\n    } = useEmojiPicker();\n    const selectedEmoji = useMemo(\n      () => data?.rows[selectedRowIndex]?.[selectedColumnIndex],\n      [data?.rows, selectedColumnIndex, selectedRowIndex]\n    );\n    const { Loading, Empty, Error, CategoryHeader, Grid, Row, Emoji } = useMemo(\n      () => ({ ...defaultContentComponents, ...components }),\n      [components]\n    );\n    const VirtuosoList = useMemo(\n      () =>\n        forwardRef<HTMLDivElement, VirtuosoListProps>(\n          ({ children, ...props }, forwardedRef) => {\n            return (\n              <div\n                role=\"grid\"\n                aria-colcount={columns}\n                {...props}\n                data-test-id={undefined}\n                ref={forwardedRef}\n              >\n                {children}\n              </div>\n            );\n          }\n        ),\n      [columns]\n    );\n    const placeholderColumns = useMemo(\n      () => Array<string>(columns).fill(\"🌫️\"),\n      [columns]\n    );\n\n    const preventDefault = useCallback((event: SyntheticEvent) => {\n      event.preventDefault();\n    }, []);\n\n    const handleEmojiPointerLeave = useCallback(() => {\n      if (interaction === \"pointer\") {\n        setInteraction(\"none\");\n      }\n    }, [interaction, setInteraction]);\n\n    useEffect(() => {\n      if (interaction === \"keyboard\") {\n        virtuosoRef.current?.scrollIntoView({\n          index: selectedRowIndex,\n          behavior: \"auto\",\n        });\n      }\n    }, [interaction, selectedRowIndex]);\n\n    return (\n      <Component {...props} ref={forwardedRef}>\n        {/* Virtualized rows are absolutely positioned so they won't make\n            the container automatically pick up their width. To achieve\n            an automatic width, we add a relative (but hidden) full row. */}\n        <div\n          style={{\n            visibility: \"hidden\",\n            height: 0,\n          }}\n        >\n          <Row attributes={placeholderRowAttributes}>\n            {placeholderColumns.map((placeholder, index) => (\n              <Emoji emoji={placeholder} key={index} />\n            ))}\n          </Row>\n        </div>\n        {isLoading ? (\n          <Loading />\n        ) : error ? (\n          <Error error={error} />\n        ) : data.count === 0 ? (\n          <Empty />\n        ) : (\n          <Grid>\n            <GroupedVirtuoso\n              ref={virtuosoRef}\n              components={{\n                Scroller: VirtuosoScroller,\n                List: VirtuosoList,\n                TopItemList: VirtuosoTopList,\n              }}\n              groupCounts={data.categoriesRowCounts}\n              groupContent={(groupIndex) => {\n                return (\n                  <CategoryHeader category={data.categories[groupIndex]} />\n                );\n              }}\n              itemContent={(rowIndex, groupIndex) => {\n                const categoryRowIndex =\n                  data.categoriesRowIndices[groupIndex].indexOf(rowIndex);\n                const categoryRowsCount = data.categoriesRowCounts[groupIndex];\n\n                return (\n                  <Row\n                    attributes={{\n                      rowIndex,\n                      categoryRowIndex,\n                      categoryRowsCount,\n                    }}\n                  >\n                    {data.rows[rowIndex].map((emoji, columnIndex) => {\n                      const isSelected =\n                        interaction !== \"none\" &&\n                        selectedColumnIndex === columnIndex &&\n                        selectedRowIndex === rowIndex;\n\n                      return (\n                        <Emoji\n                          key={emoji.emoji}\n                          role=\"gridcell\"\n                          aria-colindex={columnIndex}\n                          aria-selected={isSelected || undefined}\n                          data-selected={isSelected || undefined}\n                          onMouseDown={preventDefault}\n                          tabIndex={-1}\n                          onPointerEnter={() => {\n                            setPointerSelection(columnIndex, rowIndex);\n                          }}\n                          onPointerLeave={handleEmojiPointerLeave}\n                          onClick={(event) => {\n                            onEmojiSelect?.(emoji.emoji);\n                            event.stopPropagation();\n                          }}\n                          emoji={emoji.emoji}\n                        />\n                      );\n                    })}\n                  </Row>\n                );\n              }}\n            />\n          </Grid>\n        )}\n        {selectedEmoji && interaction !== \"none\" && (\n          <div aria-live=\"polite\" style={visuallyHidden}>\n            {selectedEmoji.name}\n          </div>\n        )}\n      </Component>\n    );\n  }\n);\n\nif (process.env.NODE_ENV !== \"production\") {\n  EmojiPickerRoot.displayName = EMOJIPICKER_ROOT_NAME;\n  EmojiPickerContent.displayName = EMOJIPICKER_CONTENT_NAME;\n  EmojiPickerSearch.displayName = EMOJIPICKER_SEARCH_NAME;\n}\n\n// NOTE: Every export from this file will be available publicly as EmojiPicker.*\nexport {\n  EmojiPickerContent as Content,\n  EmojiPickerRoot as Root,\n  EmojiPickerSearch as Search,\n};\n"],"names":[],"mappings":";;;;;;;;;;;;AAyCA;AACA;AACA;AAEA;AACA;AACA;AAkBA;AAAyB;AACb;AACD;AACT;AAEF;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACE;AACE;AAEA;AACE;AAAyB;AAC3B;AACF;AAGF;AACE;AACA;AAAqB;AAGvB;AAA4B;AAExB;AACA;AACA;AAA4B;AAC9B;AACC;AAGH;AAAsB;AAKlB;AACE;AAAA;AAGF;AAEA;AACE;AACA;AAAA;AAGF;AAEA;AAAmB;AAIf;AACE;AACA;AAEA;AACE;AACA;AAA6C;AAC/C;AAEA;AAA8C;AAGhD;AAAA;AACF;AAKE;AAEA;AACE;AACA;AAEA;AACE;AACA;AAAwB;AAC1B;AAEA;AAA8C;AAGhD;AAAA;AACF;AAKE;AAEA;AACE;AAEA;AACE;AAA6C;AAC/C;AAGF;AAAA;AACF;AAKE;AAEA;AACE;AAEA;AACE;AAAyC;AAC3C;AAGF;AAAA;AACF;AACF;AACF;AACyD;AAG3D;AACE;AACE;AAAA;AAGF;AACE;AACE;AACE;AAAA;AAGF;AAAuB;AACH;AACX;AAGT;AAAO;AACL;AACkB;AAClB;AACF;AAEF;AAAe;AAChB;AAGH;AAAqB;AAEjB;AACA;AAAa;AACf;AACa;AAGf;AAA4B;AAExB;AACE;AACA;AAAa;AAEb;AAAuB;AACzB;AACF;AACa;AAGf;AACE;AACA;AACE;AACE;AAA0B;AAC3B;AAGH;AACE;AACA;AAAiC;AACnC;AAGF;AACE;AACE;AAAe;AACjB;AAGF;AACG;AACQ;AACL;AACA;AACsB;AACtB;AACU;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAKN;AAeA;AAA0B;AAEtB;AACA;AAAM;AACJ;AACA;AACA;AACA;AACA;AAGF;AAAqB;AAEjB;AAEA;AACE;AAAA;AAGF;AACA;AACA;AAAc;AAChB;AACmC;AAGrC;AAAsB;AAElB;AACE;AAAA;AAGF;AACE;AAA2B;AAE3B;AAA4B;AAE5B;AAAyB;AAEzB;AAA2B;AAE3B;AACE;AACA;AAAmB;AACrB;AACF;AACF;AAC+C;AAGjD;AACE;AAAA;AACgE;AAChE;AAGF;AACG;AACM;AACL;AACA;AACU;AACC;AACP;AACC;AACP;AAGN;AAEA;AAA+D;AAChB;AAAQ;AAAiB;AAExB;AAAQ;AAAiB;AAEpE;AAAW;AACT;AAAe;AAClB;AAEmB;AAAQ;AAAO;AACjB;AAAQ;AAAO;AAChB;AAAQ;AAAO;AAEA;AAAQ;AAC3C;AAEA;AAAuE;AAC3D;AACQ;AAEpB;AAEA;AAAyB;AAErB;AACG;AAAQ;AAAiB;AAAkB;AAAgB;AAE5D;AAGN;AAEA;AAAwB;AAEpB;AACG;AAAQ;AAAqB;AAAgB;AAE9C;AAGN;AAyBA;AAA2B;AAEvB;AACA;AACA;AAAM;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEF;AAAsB;AACiB;AACa;AAEpD;AAAoE;AACd;AACzC;AAEb;AAAqB;AAEjB;AAEI;AACG;AACM;AACU;AACX;AACU;AACT;AAGP;AAEJ;AACF;AACM;AAEV;AAA2B;AACc;AAC/B;AAGV;AACE;AAAqB;AAGvB;AACE;AACE;AAAqB;AACvB;AAGF;AACE;AACE;AAAoC;AAC3B;AACG;AACX;AACH;AAGF;AACG;AAAc;AAAY;AAIxB;AACQ;AACO;AACJ;AACV;AAEC;AAAgB;AAEZ;AAAa;AAAkB;AAOnC;AAAM;AAKJ;AACM;AACO;AACA;AACJ;AACO;AACf;AACkB;AAEhB;AACG;AAAyC;AAAa;AAE3D;AAEE;AAEA;AAEA;AACG;AACa;AACV;AACA;AACA;AACF;AAGE;AAKA;AACG;AACY;AACN;AACU;AACc;AACA;AAChB;AACH;AAER;AAAyC;AAC3C;AACgB;AAEd;AACA;AAAsB;AACxB;AACa;AACf;AAGN;AAEJ;AAKH;AAAc;AAAgB;AAInC;AAGN;AAEA;AACE;AACA;AACA;AACF;;"}