This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch emojis-one-chart
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3b5573b7255c9152d7720602a13f50e9667173c3
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Thu Dec 4 03:00:25 2025 +0000

    feat(EmojiTextArea): add Slack-like emoji autocomplete component
    
    Introduces a new EmojiTextArea component with Slack-like emoji autocomplete
    behavior:
    
    - Triggers on `:` prefix with 2+ character minimum (configurable)
    - Smart trigger detection: colon must be preceded by whitespace, start of
      text, or another emoji (prevents false positives like URLs)
    - Prevents accidental Enter key selection when typing quickly
    - Includes 400+ curated emojis with shortcodes and keyword search
    - Fully typed with TypeScript, includes tests and Storybook stories
    
    Usage:
    ```tsx
    <EmojiTextArea
      placeholder="Type :smile: to add emojis..."
      onChange={(text) => console.log(text)}
      minCharsBeforePopup={2}
    />
    ```
    
    ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <[email protected]>
---
 .../EmojiTextArea/EmojiTextArea.stories.tsx        | 331 ++++++++++++
 .../EmojiTextArea/EmojiTextArea.test.tsx           | 170 ++++++
 .../src/components/EmojiTextArea/emojiData.ts      | 569 +++++++++++++++++++++
 .../src/components/EmojiTextArea/index.tsx         | 247 +++++++++
 .../superset-ui-core/src/components/index.ts       |   7 +
 5 files changed, 1324 insertions(+)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.stories.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.stories.tsx
new file mode 100644
index 0000000000..ad7a2665af
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.stories.tsx
@@ -0,0 +1,331 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { EmojiTextArea, type EmojiItem } from '.';
+
+const meta: Meta<typeof EmojiTextArea> = {
+  title: 'Components/EmojiTextArea',
+  component: EmojiTextArea,
+  parameters: {
+    docs: {
+      description: {
+        component: `
+A TextArea component with Slack-like emoji autocomplete.
+
+## Features
+
+- **Colon prefix trigger**: Type \`:sm\` to see smile emoji suggestions
+- **Minimum 2 characters**: Popup only shows after typing 2+ characters 
(configurable)
+- **Smart trigger detection**: Colon must be preceded by whitespace, start of 
line, or another emoji
+- **Prevents accidental selection**: Quick Enter keypress creates newline 
instead of selecting
+
+## Usage
+
+\`\`\`tsx
+import { EmojiTextArea } from '@superset-ui/core/components';
+
+<EmojiTextArea
+  placeholder="Type :smile: to add emojis..."
+  onChange={(text) => console.log(text)}
+  onEmojiSelect={(emoji) => console.log('Selected:', emoji)}
+/>
+\`\`\`
+
+## Trigger Behavior (Slack-like)
+
+The emoji picker triggers in these scenarios:
+- \`:sm\` - at the start of text
+- \`hello :sm\` - after a space
+- \`๐Ÿ˜€:sm\` - after another emoji
+
+It does NOT trigger in:
+- \`hello:sm\` - no space before colon
+- \`http://example.com\` - colon preceded by letter
+
+Try it out below!
+        `,
+      },
+    },
+  },
+  argTypes: {
+    minCharsBeforePopup: {
+      control: { type: 'number', min: 1, max: 5 },
+      description: 'Minimum characters after colon before showing popup',
+      defaultValue: 2,
+    },
+    maxSuggestions: {
+      control: { type: 'number', min: 1, max: 20 },
+      description: 'Maximum number of emoji suggestions to show',
+      defaultValue: 10,
+    },
+    placeholder: {
+      control: 'text',
+      description: 'Placeholder text',
+    },
+    rows: {
+      control: { type: 'number', min: 1, max: 20 },
+      description: 'Number of visible rows',
+    },
+  },
+};
+
+export default meta;
+type Story = StoryObj<typeof EmojiTextArea>;
+
+export const Default: Story = {
+  args: {
+    placeholder: 'Type :smile: or :thumbsup: to add emojis...',
+    rows: 4,
+    style: { width: '100%', maxWidth: 500 },
+  },
+};
+
+export const WithMinChars: Story = {
+  args: {
+    ...Default.args,
+    minCharsBeforePopup: 3,
+    placeholder: 'Requires 3 characters after colon (e.g., :smi)',
+  },
+};
+
+export const WithMaxSuggestions: Story = {
+  args: {
+    ...Default.args,
+    maxSuggestions: 5,
+    placeholder: 'Shows max 5 suggestions',
+  },
+};
+
+export const Controlled: Story = {
+  render: function ControlledStory() {
+    const [value, setValue] = useState('');
+    const [selectedEmojis, setSelectedEmojis] = useState<EmojiItem[]>([]);
+
+    return (
+      <div style={{ maxWidth: 500 }}>
+        <EmojiTextArea
+          value={value}
+          onChange={setValue}
+          onEmojiSelect={emoji => setSelectedEmojis(prev => [...prev, emoji])}
+          placeholder="Type :smile: or :heart: to add emojis..."
+          rows={4}
+          style={{ width: '100%' }}
+        />
+        <div style={{ marginTop: 16 }}>
+          <strong>Current value:</strong>
+          <pre
+            style={{
+              background: 'var(--ant-color-bg-container)',
+              padding: 8,
+              borderRadius: 4,
+              border: '1px solid var(--ant-color-border)',
+              whiteSpace: 'pre-wrap',
+              wordBreak: 'break-word',
+            }}
+          >
+            {value || '(empty)'}
+          </pre>
+        </div>
+        {selectedEmojis.length > 0 && (
+          <div style={{ marginTop: 16 }}>
+            <strong>Selected emojis:</strong>
+            <div style={{ fontSize: 24, marginTop: 8 }}>
+              {selectedEmojis.map((e, i) => (
+                <span key={i} title={`:${e.shortcode}:`}>
+                  {e.emoji}
+                </span>
+              ))}
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  },
+};
+
+export const SlackBehaviorDemo: Story = {
+  render: function SlackBehaviorDemoStory() {
+    const examples = [
+      { input: ':sm', works: true, desc: 'Start of text' },
+      { input: 'hello :sm', works: true, desc: 'After space' },
+      {
+        input: '๐Ÿ˜€:sm',
+        works: true,
+        desc: 'After emoji',
+        needsEmoji: true,
+      },
+      { input: 'hello:sm', works: false, desc: 'No space before colon' },
+      { input: ':s', works: false, desc: 'Only 1 character' },
+    ];
+
+    return (
+      <div style={{ maxWidth: 600 }}>
+        <h3>Slack-like Trigger Behavior</h3>
+        <p style={{ color: 'var(--ant-color-text-secondary)' }}>
+          The emoji picker mimics Slack&apos;s behavior. Try these examples:
+        </p>
+
+        <table
+          style={{
+            width: '100%',
+            borderCollapse: 'collapse',
+            marginBottom: 24,
+          }}
+        >
+          <thead>
+            <tr>
+              <th
+                style={{
+                  textAlign: 'left',
+                  padding: 8,
+                  borderBottom: '1px solid var(--ant-color-border)',
+                }}
+              >
+                Input
+              </th>
+              <th
+                style={{
+                  textAlign: 'left',
+                  padding: 8,
+                  borderBottom: '1px solid var(--ant-color-border)',
+                }}
+              >
+                Shows Popup?
+              </th>
+              <th
+                style={{
+                  textAlign: 'left',
+                  padding: 8,
+                  borderBottom: '1px solid var(--ant-color-border)',
+                }}
+              >
+                Reason
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            {examples.map((ex, i) => (
+              <tr key={i}>
+                <td
+                  style={{
+                    padding: 8,
+                    borderBottom: '1px solid var(--ant-color-border)',
+                    fontFamily: 'monospace',
+                  }}
+                >
+                  {ex.input}
+                </td>
+                <td
+                  style={{
+                    padding: 8,
+                    borderBottom: '1px solid var(--ant-color-border)',
+                  }}
+                >
+                  {ex.works ? 'โœ… Yes' : 'โŒ No'}
+                </td>
+                <td
+                  style={{
+                    padding: 8,
+                    borderBottom: '1px solid var(--ant-color-border)',
+                  }}
+                >
+                  {ex.desc}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+
+        <EmojiTextArea
+          placeholder="Try the examples above..."
+          rows={4}
+          style={{ width: '100%' }}
+        />
+      </div>
+    );
+  },
+};
+
+export const InForm: Story = {
+  render: function InFormStory() {
+    const [description, setDescription] = useState('');
+    const [title, setTitle] = useState('');
+
+    const handleSubmit = (e: React.FormEvent) => {
+      e.preventDefault();
+      // eslint-disable-next-line no-alert
+      alert(`Title: ${title}\nDescription: ${description}`);
+    };
+
+    return (
+      <form onSubmit={handleSubmit} style={{ maxWidth: 500 }}>
+        <div style={{ marginBottom: 16 }}>
+          <label htmlFor="title" style={{ display: 'block', marginBottom: 4 }}>
+            Title
+          </label>
+          <input
+            id="title"
+            type="text"
+            value={title}
+            onChange={e => setTitle(e.target.value)}
+            placeholder="Enter a title"
+            style={{
+              width: '100%',
+              padding: 8,
+              borderRadius: 4,
+              border: '1px solid var(--ant-color-border)',
+            }}
+          />
+        </div>
+
+        <div style={{ marginBottom: 16 }}>
+          <label
+            htmlFor="description"
+            style={{ display: 'block', marginBottom: 4 }}
+          >
+            Description (with emoji support)
+          </label>
+          <EmojiTextArea
+            id="description"
+            value={description}
+            onChange={setDescription}
+            placeholder="Add a description... use :smile: for emojis!"
+            rows={4}
+            style={{ width: '100%' }}
+          />
+        </div>
+
+        <button
+          type="submit"
+          style={{
+            padding: '8px 16px',
+            background: 'var(--ant-color-primary)',
+            color: 'white',
+            border: 'none',
+            borderRadius: 4,
+            cursor: 'pointer',
+          }}
+        >
+          Submit
+        </button>
+      </form>
+    );
+  },
+};
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.test.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.test.tsx
new file mode 100644
index 0000000000..f7c29600a3
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.test.tsx
@@ -0,0 +1,170 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen, userEvent } from '@superset-ui/core/spec';
+import { EmojiTextArea } from '.';
+import { filterEmojis, EMOJI_DATA } from './emojiData';
+
+test('renders EmojiTextArea with placeholder', () => {
+  render(<EmojiTextArea placeholder="Type something..." />);
+  expect(screen.getByPlaceholderText('Type something...')).toBeInTheDocument();
+});
+
+test('renders EmojiTextArea as textarea element', () => {
+  render(<EmojiTextArea placeholder="Type here" />);
+  const textarea = screen.getByPlaceholderText('Type here');
+  expect(textarea.tagName.toLowerCase()).toBe('textarea');
+});
+
+test('allows typing in the textarea', async () => {
+  render(<EmojiTextArea placeholder="Type here" />);
+  const textarea = screen.getByPlaceholderText('Type here');
+  await userEvent.type(textarea, 'Hello world');
+  expect(textarea).toHaveValue('Hello world');
+});
+
+test('calls onChange when typing', async () => {
+  const onChange = jest.fn();
+  render(<EmojiTextArea placeholder="Type here" onChange={onChange} />);
+  const textarea = screen.getByPlaceholderText('Type here');
+  await userEvent.type(textarea, 'Hi');
+  expect(onChange).toHaveBeenCalled();
+});
+
+test('passes through rows prop', () => {
+  render(<EmojiTextArea placeholder="Type here" rows={5} />);
+  const textarea = screen.getByPlaceholderText('Type here');
+  expect(textarea).toHaveAttribute('rows', '5');
+});
+
+test('forwards ref to underlying component', () => {
+  const ref = { current: null };
+  render(<EmojiTextArea ref={ref} placeholder="Type here" />);
+  expect(ref.current).not.toBeNull();
+});
+
+test('renders controlled component with value prop', () => {
+  render(<EmojiTextArea value="Hello" onChange={() => {}} />);
+  expect(screen.getByDisplayValue('Hello')).toBeInTheDocument();
+});
+
+// ============================================
+// Unit tests for filterEmojis utility function
+// ============================================
+
+test('filterEmojis returns matching emojis by shortcode', () => {
+  const results = filterEmojis('smile');
+  expect(results.length).toBeGreaterThan(0);
+  expect(results[0].shortcode).toBe('smile');
+});
+
+test('filterEmojis returns matching emojis by partial shortcode', () => {
+  const results = filterEmojis('sm');
+  expect(results.length).toBeGreaterThan(0);
+  // Should include smile, smirk, etc.
+  expect(results.some(e => e.shortcode.includes('sm'))).toBe(true);
+});
+
+test('filterEmojis returns matching emojis by keyword', () => {
+  const results = filterEmojis('happy');
+  expect(results.length).toBeGreaterThan(0);
+  // Should include emojis with 'happy' keyword
+  expect(results.some(e => e.keywords?.includes('happy'))).toBe(true);
+});
+
+test('filterEmojis is case insensitive', () => {
+  const results1 = filterEmojis('SMILE');
+  const results2 = filterEmojis('smile');
+  expect(results1.length).toBe(results2.length);
+  expect(results1[0].shortcode).toBe(results2[0].shortcode);
+});
+
+test('filterEmojis respects limit parameter', () => {
+  const results = filterEmojis('a', 5);
+  expect(results.length).toBeLessThanOrEqual(5);
+});
+
+test('filterEmojis returns empty array for empty search', () => {
+  const results = filterEmojis('');
+  expect(results).toEqual([]);
+});
+
+test('filterEmojis returns empty array for no matches', () => {
+  const results = filterEmojis('zzzznotanemoji');
+  expect(results).toEqual([]);
+});
+
+// ============================================
+// Unit tests for EMOJI_DATA
+// ============================================
+
+test('EMOJI_DATA contains expected smileys', () => {
+  const smile = EMOJI_DATA.find(e => e.shortcode === 'smile');
+  expect(smile).toBeDefined();
+  expect(smile?.emoji).toBe('๐Ÿ˜„');
+
+  const joy = EMOJI_DATA.find(e => e.shortcode === 'joy');
+  expect(joy).toBeDefined();
+  expect(joy?.emoji).toBe('๐Ÿ˜‚');
+});
+
+test('EMOJI_DATA contains expected gestures', () => {
+  const thumbsup = EMOJI_DATA.find(e => e.shortcode === 'thumbsup');
+  expect(thumbsup).toBeDefined();
+  expect(thumbsup?.emoji).toBe('๐Ÿ‘');
+
+  const clap = EMOJI_DATA.find(e => e.shortcode === 'clap');
+  expect(clap).toBeDefined();
+  expect(clap?.emoji).toBe('๐Ÿ‘');
+});
+
+test('EMOJI_DATA contains expected symbols', () => {
+  const heart = EMOJI_DATA.find(e => e.shortcode === 'heart');
+  expect(heart).toBeDefined();
+  expect(heart?.emoji).toBe('โค๏ธ');
+
+  const fire = EMOJI_DATA.find(e => e.shortcode === 'fire');
+  expect(fire).toBeDefined();
+  expect(fire?.emoji).toBe('๐Ÿ”ฅ');
+
+  const checkmark = EMOJI_DATA.find(e => e.shortcode === 'white_check_mark');
+  expect(checkmark).toBeDefined();
+  expect(checkmark?.emoji).toBe('โœ…');
+});
+
+test('EMOJI_DATA items have required properties', () => {
+  EMOJI_DATA.forEach(item => {
+    expect(item).toHaveProperty('shortcode');
+    expect(item).toHaveProperty('emoji');
+    expect(typeof item.shortcode).toBe('string');
+    expect(typeof item.emoji).toBe('string');
+    expect(item.shortcode.length).toBeGreaterThan(0);
+    expect(item.emoji.length).toBeGreaterThan(0);
+  });
+});
+
+test('EMOJI_DATA shortcodes are unique', () => {
+  const shortcodes = EMOJI_DATA.map(e => e.shortcode);
+  const uniqueShortcodes = new Set(shortcodes);
+  expect(uniqueShortcodes.size).toBe(shortcodes.length);
+});
+
+test('EMOJI_DATA has a reasonable number of emojis', () => {
+  // Ensure we have a substantial emoji set
+  expect(EMOJI_DATA.length).toBeGreaterThan(100);
+});
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/emojiData.ts
 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/emojiData.ts
new file mode 100644
index 0000000000..a0048dcfc7
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/emojiData.ts
@@ -0,0 +1,569 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface EmojiItem {
+  shortcode: string;
+  emoji: string;
+  keywords?: string[];
+}
+
+/**
+ * Common emoji data with shortcodes.
+ * This is a curated subset of emojis commonly used in Slack-like applications.
+ * Can be extended or replaced with a more comprehensive emoji library.
+ */
+export const EMOJI_DATA: EmojiItem[] = [
+  // Smileys & Emotion
+  { shortcode: 'smile', emoji: '๐Ÿ˜„', keywords: ['happy', 'joy', 'glad'] },
+  { shortcode: 'smiley', emoji: '๐Ÿ˜ƒ', keywords: ['happy', 'joy'] },
+  { shortcode: 'grinning', emoji: '๐Ÿ˜€', keywords: ['happy', 'smile'] },
+  { shortcode: 'blush', emoji: '๐Ÿ˜Š', keywords: ['happy', 'shy', 'smile'] },
+  { shortcode: 'wink', emoji: '๐Ÿ˜‰', keywords: ['flirt'] },
+  {
+    shortcode: 'heart_eyes',
+    emoji: '๐Ÿ˜',
+    keywords: ['love', 'crush', 'adore'],
+  },
+  { shortcode: 'kissing_heart', emoji: '๐Ÿ˜˜', keywords: ['love', 'kiss'] },
+  { shortcode: 'laughing', emoji: '๐Ÿ˜†', keywords: ['happy', 'haha', 'lol'] },
+  { shortcode: 'sweat_smile', emoji: '๐Ÿ˜…', keywords: ['nervous', 'phew'] },
+  { shortcode: 'joy', emoji: '๐Ÿ˜‚', keywords: ['tears', 'laugh', 'lol', 'lmao'] 
},
+  {
+    shortcode: 'rofl',
+    emoji: '๐Ÿคฃ',
+    keywords: ['rolling', 'laugh', 'lol', 'lmao'],
+  },
+  { shortcode: 'relaxed', emoji: 'โ˜บ๏ธ', keywords: ['calm', 'peace'] },
+  { shortcode: 'yum', emoji: '๐Ÿ˜‹', keywords: ['tasty', 'delicious'] },
+  { shortcode: 'relieved', emoji: '๐Ÿ˜Œ', keywords: ['calm', 'peaceful'] },
+  { shortcode: 'sunglasses', emoji: '๐Ÿ˜Ž', keywords: ['cool', 'awesome'] },
+  { shortcode: 'smirk', emoji: '๐Ÿ˜', keywords: ['sly', 'confident'] },
+  { shortcode: 'neutral_face', emoji: '๐Ÿ˜', keywords: ['meh', 'blank'] },
+  { shortcode: 'expressionless', emoji: '๐Ÿ˜‘', keywords: ['blank', 'meh'] },
+  { shortcode: 'unamused', emoji: '๐Ÿ˜’', keywords: ['bored', 'meh'] },
+  { shortcode: 'sweat', emoji: '๐Ÿ˜“', keywords: ['nervous', 'worried'] },
+  { shortcode: 'pensive', emoji: '๐Ÿ˜”', keywords: ['sad', 'thoughtful'] },
+  { shortcode: 'confused', emoji: '๐Ÿ˜•', keywords: ['puzzled', 'unsure'] },
+  { shortcode: 'upside_down', emoji: '๐Ÿ™ƒ', keywords: ['silly', 'sarcasm'] },
+  { shortcode: 'thinking', emoji: '๐Ÿค”', keywords: ['ponder', 'hmm'] },
+  { shortcode: 'zipper_mouth', emoji: '๐Ÿค', keywords: ['secret', 'quiet'] },
+  { shortcode: 'raised_eyebrow', emoji: '๐Ÿคจ', keywords: ['skeptical', 'doubt'] 
},
+  { shortcode: 'rolling_eyes', emoji: '๐Ÿ™„', keywords: ['annoyed', 'whatever'] },
+  { shortcode: 'grimacing', emoji: '๐Ÿ˜ฌ', keywords: ['awkward', 'nervous'] },
+  { shortcode: 'lying_face', emoji: '๐Ÿคฅ', keywords: ['liar', 'pinocchio'] },
+  { shortcode: 'shushing', emoji: '๐Ÿคซ', keywords: ['quiet', 'secret'] },
+  { shortcode: 'hand_over_mouth', emoji: '๐Ÿคญ', keywords: ['oops', 'giggle'] },
+  { shortcode: 'face_vomiting', emoji: '๐Ÿคฎ', keywords: ['sick', 'gross'] },
+  { shortcode: 'exploding_head', emoji: '๐Ÿคฏ', keywords: ['mind', 'blown'] },
+  { shortcode: 'cowboy', emoji: '๐Ÿค ', keywords: ['western', 'yeehaw'] },
+  { shortcode: 'partying', emoji: '๐Ÿฅณ', keywords: ['party', 'celebration'] },
+  { shortcode: 'star_struck', emoji: '๐Ÿคฉ', keywords: ['excited', 'amazed'] },
+  { shortcode: 'sleeping', emoji: '๐Ÿ˜ด', keywords: ['zzz', 'tired'] },
+  { shortcode: 'drooling', emoji: '๐Ÿคค', keywords: ['hungry', 'want'] },
+  { shortcode: 'sleepy', emoji: '๐Ÿ˜ช', keywords: ['tired', 'zzz'] },
+  { shortcode: 'mask', emoji: '๐Ÿ˜ท', keywords: ['sick', 'covid'] },
+  { shortcode: 'nerd', emoji: '๐Ÿค“', keywords: ['geek', 'smart'] },
+  { shortcode: 'monocle', emoji: '๐Ÿง', keywords: ['curious', 'inspect'] },
+  { shortcode: 'worried', emoji: '๐Ÿ˜Ÿ', keywords: ['concerned', 'anxious'] },
+  { shortcode: 'frowning', emoji: '๐Ÿ™', keywords: ['sad', 'unhappy'] },
+  { shortcode: 'open_mouth', emoji: '๐Ÿ˜ฎ', keywords: ['surprised', 'wow'] },
+  { shortcode: 'hushed', emoji: '๐Ÿ˜ฏ', keywords: ['surprised', 'quiet'] },
+  { shortcode: 'astonished', emoji: '๐Ÿ˜ฒ', keywords: ['shocked', 'wow'] },
+  { shortcode: 'flushed', emoji: '๐Ÿ˜ณ', keywords: ['embarrassed', 'shy'] },
+  { shortcode: 'pleading', emoji: '๐Ÿฅบ', keywords: ['puppy', 'please'] },
+  { shortcode: 'cry', emoji: '๐Ÿ˜ข', keywords: ['sad', 'tear'] },
+  { shortcode: 'sob', emoji: '๐Ÿ˜ญ', keywords: ['crying', 'sad', 'tears'] },
+  { shortcode: 'scream', emoji: '๐Ÿ˜ฑ', keywords: ['scared', 'horror'] },
+  { shortcode: 'confounded', emoji: '๐Ÿ˜–', keywords: ['frustrated'] },
+  { shortcode: 'persevere', emoji: '๐Ÿ˜ฃ', keywords: ['struggling'] },
+  { shortcode: 'disappointed', emoji: '๐Ÿ˜ž', keywords: ['sad', 'let down'] },
+  { shortcode: 'fearful', emoji: '๐Ÿ˜จ', keywords: ['scared', 'afraid'] },
+  { shortcode: 'cold_sweat', emoji: '๐Ÿ˜ฐ', keywords: ['nervous', 'anxious'] },
+  { shortcode: 'weary', emoji: '๐Ÿ˜ฉ', keywords: ['tired', 'exhausted'] },
+  { shortcode: 'tired_face', emoji: '๐Ÿ˜ซ', keywords: ['exhausted'] },
+  { shortcode: 'angry', emoji: '๐Ÿ˜ ', keywords: ['mad', 'grumpy'] },
+  { shortcode: 'rage', emoji: '๐Ÿ˜ก', keywords: ['angry', 'furious'] },
+  { shortcode: 'triumph', emoji: '๐Ÿ˜ค', keywords: ['proud', 'huffing'] },
+  { shortcode: 'skull', emoji: '๐Ÿ’€', keywords: ['dead', 'death'] },
+  { shortcode: 'poop', emoji: '๐Ÿ’ฉ', keywords: ['crap', 'shit'] },
+  { shortcode: 'clown', emoji: '๐Ÿคก', keywords: ['funny', 'circus'] },
+  { shortcode: 'imp', emoji: '๐Ÿ‘ฟ', keywords: ['devil', 'evil'] },
+  { shortcode: 'ghost', emoji: '๐Ÿ‘ป', keywords: ['boo', 'spooky'] },
+  { shortcode: 'alien', emoji: '๐Ÿ‘ฝ', keywords: ['ufo', 'space'] },
+  { shortcode: 'robot', emoji: '๐Ÿค–', keywords: ['bot', 'machine'] },
+  { shortcode: 'cat', emoji: '๐Ÿ˜บ', keywords: ['kitty', 'meow'] },
+  { shortcode: 'heart_eyes_cat', emoji: '๐Ÿ˜ป', keywords: ['love', 'cat'] },
+  { shortcode: 'joy_cat', emoji: '๐Ÿ˜น', keywords: ['laugh', 'cat'] },
+  { shortcode: 'crying_cat', emoji: '๐Ÿ˜ฟ', keywords: ['sad', 'cat'] },
+  { shortcode: 'pouting_cat', emoji: '๐Ÿ˜พ', keywords: ['angry', 'cat'] },
+  { shortcode: 'see_no_evil', emoji: '๐Ÿ™ˆ', keywords: ['monkey', 'shy'] },
+  { shortcode: 'hear_no_evil', emoji: '๐Ÿ™‰', keywords: ['monkey'] },
+  { shortcode: 'speak_no_evil', emoji: '๐Ÿ™Š', keywords: ['monkey', 'secret'] },
+
+  // Gestures & Body
+  { shortcode: 'wave', emoji: '๐Ÿ‘‹', keywords: ['hello', 'bye', 'hi'] },
+  { shortcode: 'raised_hand', emoji: 'โœ‹', keywords: ['stop', 'high five'] },
+  { shortcode: 'ok_hand', emoji: '๐Ÿ‘Œ', keywords: ['perfect', 'nice'] },
+  { shortcode: 'pinching_hand', emoji: '๐Ÿค', keywords: ['small', 'tiny'] },
+  { shortcode: 'v', emoji: 'โœŒ๏ธ', keywords: ['peace', 'victory'] },
+  { shortcode: 'crossed_fingers', emoji: '๐Ÿคž', keywords: ['luck', 'hope'] },
+  { shortcode: 'love_you', emoji: '๐ŸคŸ', keywords: ['ily', 'sign'] },
+  { shortcode: 'metal', emoji: '๐Ÿค˜', keywords: ['rock', 'horns'] },
+  { shortcode: 'call_me', emoji: '๐Ÿค™', keywords: ['phone', 'shaka'] },
+  { shortcode: 'point_left', emoji: '๐Ÿ‘ˆ', keywords: ['direction'] },
+  { shortcode: 'point_right', emoji: '๐Ÿ‘‰', keywords: ['direction'] },
+  { shortcode: 'point_up', emoji: '๐Ÿ‘†', keywords: ['direction'] },
+  { shortcode: 'point_down', emoji: '๐Ÿ‘‡', keywords: ['direction'] },
+  { shortcode: 'middle_finger', emoji: '๐Ÿ–•', keywords: ['flip', 'rude'] },
+  { shortcode: 'thumbsup', emoji: '๐Ÿ‘', keywords: ['yes', 'good', '+1'] },
+  { shortcode: 'thumbsdown', emoji: '๐Ÿ‘Ž', keywords: ['no', 'bad', '-1'] },
+  { shortcode: 'fist', emoji: 'โœŠ', keywords: ['power', 'punch'] },
+  { shortcode: 'punch', emoji: '๐Ÿ‘Š', keywords: ['fist', 'bump'] },
+  { shortcode: 'clap', emoji: '๐Ÿ‘', keywords: ['applause', 'bravo'] },
+  { shortcode: 'raised_hands', emoji: '๐Ÿ™Œ', keywords: ['celebration', 'yay'] },
+  { shortcode: 'open_hands', emoji: '๐Ÿ‘', keywords: ['hug', 'open'] },
+  { shortcode: 'palms_up', emoji: '๐Ÿคฒ', keywords: ['prayer', 'request'] },
+  { shortcode: 'handshake', emoji: '๐Ÿค', keywords: ['deal', 'agreement'] },
+  { shortcode: 'pray', emoji: '๐Ÿ™', keywords: ['please', 'thanks', 'namaste'] },
+  { shortcode: 'writing', emoji: 'โœ๏ธ', keywords: ['write', 'pen'] },
+  { shortcode: 'nail_care', emoji: '๐Ÿ’…', keywords: ['nails', 'fabulous'] },
+  { shortcode: 'selfie', emoji: '๐Ÿคณ', keywords: ['photo', 'camera'] },
+  { shortcode: 'muscle', emoji: '๐Ÿ’ช', keywords: ['strong', 'flex', 'bicep'] },
+  { shortcode: 'leg', emoji: '๐Ÿฆต', keywords: ['kick'] },
+  { shortcode: 'foot', emoji: '๐Ÿฆถ', keywords: ['kick', 'step'] },
+  { shortcode: 'ear', emoji: '๐Ÿ‘‚', keywords: ['listen', 'hear'] },
+  { shortcode: 'nose', emoji: '๐Ÿ‘ƒ', keywords: ['smell', 'sniff'] },
+  { shortcode: 'brain', emoji: '๐Ÿง ', keywords: ['think', 'smart'] },
+  { shortcode: 'eyes', emoji: '๐Ÿ‘€', keywords: ['look', 'see', 'watch'] },
+  { shortcode: 'eye', emoji: '๐Ÿ‘๏ธ', keywords: ['look', 'see'] },
+  { shortcode: 'tongue', emoji: '๐Ÿ‘…', keywords: ['taste', 'lick'] },
+  { shortcode: 'lips', emoji: '๐Ÿ‘„', keywords: ['mouth', 'kiss'] },
+  { shortcode: 'baby', emoji: '๐Ÿ‘ถ', keywords: ['child', 'infant'] },
+  { shortcode: 'person', emoji: '๐Ÿง‘', keywords: ['human', 'adult'] },
+  { shortcode: 'man', emoji: '๐Ÿ‘จ', keywords: ['male', 'guy'] },
+  { shortcode: 'woman', emoji: '๐Ÿ‘ฉ', keywords: ['female', 'lady'] },
+  { shortcode: 'older_person', emoji: '๐Ÿง“', keywords: ['senior', 'elderly'] },
+
+  // Hearts & Love
+  { shortcode: 'heart', emoji: 'โค๏ธ', keywords: ['love', 'red'] },
+  { shortcode: 'orange_heart', emoji: '๐Ÿงก', keywords: ['love'] },
+  { shortcode: 'yellow_heart', emoji: '๐Ÿ’›', keywords: ['love'] },
+  { shortcode: 'green_heart', emoji: '๐Ÿ’š', keywords: ['love'] },
+  { shortcode: 'blue_heart', emoji: '๐Ÿ’™', keywords: ['love'] },
+  { shortcode: 'purple_heart', emoji: '๐Ÿ’œ', keywords: ['love'] },
+  { shortcode: 'black_heart', emoji: '๐Ÿ–ค', keywords: ['love', 'dark'] },
+  { shortcode: 'white_heart', emoji: '๐Ÿค', keywords: ['love', 'pure'] },
+  { shortcode: 'brown_heart', emoji: '๐ŸคŽ', keywords: ['love'] },
+  { shortcode: 'broken_heart', emoji: '๐Ÿ’”', keywords: ['sad', 'heartbreak'] },
+  { shortcode: 'heartbeat', emoji: '๐Ÿ’“', keywords: ['love', 'pulse'] },
+  { shortcode: 'heartpulse', emoji: '๐Ÿ’—', keywords: ['love', 'growing'] },
+  { shortcode: 'two_hearts', emoji: '๐Ÿ’•', keywords: ['love', 'romance'] },
+  { shortcode: 'revolving_hearts', emoji: '๐Ÿ’ž', keywords: ['love'] },
+  { shortcode: 'cupid', emoji: '๐Ÿ’˜', keywords: ['love', 'arrow'] },
+  { shortcode: 'sparkling_heart', emoji: '๐Ÿ’–', keywords: ['love', 'sparkle'] },
+  { shortcode: 'gift_heart', emoji: '๐Ÿ’', keywords: ['love', 'valentine'] },
+  { shortcode: 'heart_decoration', emoji: '๐Ÿ’Ÿ', keywords: ['love'] },
+  { shortcode: 'kiss', emoji: '๐Ÿ’‹', keywords: ['love', 'lips'] },
+  { shortcode: 'love_letter', emoji: '๐Ÿ’Œ', keywords: ['email', 'message'] },
+
+  // Symbols & Objects
+  { shortcode: 'fire', emoji: '๐Ÿ”ฅ', keywords: ['hot', 'lit', 'flame'] },
+  { shortcode: 'star', emoji: 'โญ', keywords: ['favorite', 'rating'] },
+  { shortcode: 'sparkles', emoji: 'โœจ', keywords: ['shiny', 'new', 'magic'] },
+  { shortcode: 'zap', emoji: 'โšก', keywords: ['lightning', 'power'] },
+  { shortcode: 'boom', emoji: '๐Ÿ’ฅ', keywords: ['explosion', 'collision'] },
+  { shortcode: 'dizzy', emoji: '๐Ÿ’ซ', keywords: ['star', 'dazed'] },
+  { shortcode: 'speech_balloon', emoji: '๐Ÿ’ฌ', keywords: ['talk', 'chat'] },
+  { shortcode: 'thought_balloon', emoji: '๐Ÿ’ญ', keywords: ['think', 'idea'] },
+  { shortcode: 'zzz', emoji: '๐Ÿ’ค', keywords: ['sleep', 'tired'] },
+  { shortcode: 'wave_emoji', emoji: '๐ŸŒŠ', keywords: ['ocean', 'water'] },
+  { shortcode: 'droplet', emoji: '๐Ÿ’ง', keywords: ['water', 'sweat'] },
+  { shortcode: 'sweat_drops', emoji: '๐Ÿ’ฆ', keywords: ['water', 'splash'] },
+  { shortcode: 'dash', emoji: '๐Ÿ’จ', keywords: ['wind', 'running'] },
+  { shortcode: 'hole', emoji: '๐Ÿ•ณ๏ธ', keywords: ['empty', 'void'] },
+  { shortcode: 'bomb', emoji: '๐Ÿ’ฃ', keywords: ['explosive', 'danger'] },
+  { shortcode: 'money', emoji: '๐Ÿ’ฐ', keywords: ['bag', 'cash', 'dollar'] },
+  { shortcode: 'dollar', emoji: '๐Ÿ’ต', keywords: ['money', 'cash'] },
+  { shortcode: 'gem', emoji: '๐Ÿ’Ž', keywords: ['diamond', 'jewel'] },
+  { shortcode: 'bulb', emoji: '๐Ÿ’ก', keywords: ['idea', 'light'] },
+  { shortcode: 'bell', emoji: '๐Ÿ””', keywords: ['notification', 'alert'] },
+  { shortcode: 'loudspeaker', emoji: '๐Ÿ“ข', keywords: ['announce'] },
+  { shortcode: 'mega', emoji: '๐Ÿ“ฃ', keywords: ['megaphone', 'announce'] },
+  { shortcode: 'lock', emoji: '๐Ÿ”’', keywords: ['secure', 'closed'] },
+  { shortcode: 'unlock', emoji: '๐Ÿ”“', keywords: ['open', 'access'] },
+  { shortcode: 'key', emoji: '๐Ÿ”‘', keywords: ['password', 'access'] },
+  { shortcode: 'magnifying_glass', emoji: '๐Ÿ”', keywords: ['search', 'find'] },
+  { shortcode: 'link', emoji: '๐Ÿ”—', keywords: ['chain', 'url'] },
+  { shortcode: 'paperclip', emoji: '๐Ÿ“Ž', keywords: ['attach'] },
+  { shortcode: 'scissors', emoji: 'โœ‚๏ธ', keywords: ['cut', 'snip'] },
+  { shortcode: 'hammer', emoji: '๐Ÿ”จ', keywords: ['tool', 'build'] },
+  { shortcode: 'wrench', emoji: '๐Ÿ”ง', keywords: ['tool', 'fix'] },
+  { shortcode: 'gear', emoji: 'โš™๏ธ', keywords: ['settings', 'cog'] },
+  { shortcode: 'shield', emoji: '๐Ÿ›ก๏ธ', keywords: ['protect', 'security'] },
+  { shortcode: 'trophy', emoji: '๐Ÿ†', keywords: ['win', 'first', 'award'] },
+  { shortcode: 'medal', emoji: '๐Ÿ…', keywords: ['award', 'sports'] },
+  { shortcode: 'first_place', emoji: '๐Ÿฅ‡', keywords: ['gold', 'winner'] },
+  { shortcode: 'second_place', emoji: '๐Ÿฅˆ', keywords: ['silver'] },
+  { shortcode: 'third_place', emoji: '๐Ÿฅ‰', keywords: ['bronze'] },
+  { shortcode: 'soccer', emoji: 'โšฝ', keywords: ['football', 'sports'] },
+  { shortcode: 'basketball', emoji: '๐Ÿ€', keywords: ['sports', 'ball'] },
+  { shortcode: 'football', emoji: '๐Ÿˆ', keywords: ['sports', 'american'] },
+  { shortcode: 'baseball', emoji: 'โšพ', keywords: ['sports', 'ball'] },
+  { shortcode: 'tennis', emoji: '๐ŸŽพ', keywords: ['sports', 'ball'] },
+  { shortcode: 'dart', emoji: '๐ŸŽฏ', keywords: ['target', 'bullseye'] },
+  { shortcode: 'video_game', emoji: '๐ŸŽฎ', keywords: ['gaming', 'controller'] },
+  { shortcode: 'slot_machine', emoji: '๐ŸŽฐ', keywords: ['gambling', 'casino'] },
+  { shortcode: 'game_die', emoji: '๐ŸŽฒ', keywords: ['dice', 'random'] },
+  { shortcode: 'jigsaw', emoji: '๐Ÿงฉ', keywords: ['puzzle', 'piece'] },
+  { shortcode: 'art', emoji: '๐ŸŽจ', keywords: ['palette', 'paint'] },
+  { shortcode: 'performing_arts', emoji: '๐ŸŽญ', keywords: ['theater', 'drama'] },
+  { shortcode: 'microphone', emoji: '๐ŸŽค', keywords: ['sing', 'karaoke'] },
+  { shortcode: 'headphones', emoji: '๐ŸŽง', keywords: ['music', 'audio'] },
+  { shortcode: 'musical_note', emoji: '๐ŸŽต', keywords: ['music', 'song'] },
+  { shortcode: 'notes', emoji: '๐ŸŽถ', keywords: ['music', 'melody'] },
+  { shortcode: 'guitar', emoji: '๐ŸŽธ', keywords: ['music', 'rock'] },
+  { shortcode: 'piano', emoji: '๐ŸŽน', keywords: ['music', 'keys'] },
+  { shortcode: 'drum', emoji: '๐Ÿฅ', keywords: ['music', 'beat'] },
+  { shortcode: 'trumpet', emoji: '๐ŸŽบ', keywords: ['music', 'brass'] },
+  { shortcode: 'violin', emoji: '๐ŸŽป', keywords: ['music', 'string'] },
+  { shortcode: 'movie_camera', emoji: '๐ŸŽฅ', keywords: ['film', 'video'] },
+  { shortcode: 'camera', emoji: '๐Ÿ“ท', keywords: ['photo', 'picture'] },
+  { shortcode: 'tv', emoji: '๐Ÿ“บ', keywords: ['television', 'watch'] },
+  { shortcode: 'computer', emoji: '๐Ÿ’ป', keywords: ['laptop', 'pc'] },
+  { shortcode: 'keyboard', emoji: 'โŒจ๏ธ', keywords: ['type', 'computer'] },
+  { shortcode: 'phone', emoji: '๐Ÿ“ฑ', keywords: ['mobile', 'cell'] },
+  { shortcode: 'email', emoji: '๐Ÿ“ง', keywords: ['mail', 'message'] },
+  { shortcode: 'inbox', emoji: '๐Ÿ“ฅ', keywords: ['mail', 'receive'] },
+  { shortcode: 'outbox', emoji: '๐Ÿ“ค', keywords: ['mail', 'send'] },
+  { shortcode: 'package', emoji: '๐Ÿ“ฆ', keywords: ['box', 'delivery'] },
+  { shortcode: 'memo', emoji: '๐Ÿ“', keywords: ['note', 'write'] },
+  { shortcode: 'page', emoji: '๐Ÿ“„', keywords: ['document', 'file'] },
+  { shortcode: 'bookmark', emoji: '๐Ÿ”–', keywords: ['save', 'tag'] },
+  { shortcode: 'book', emoji: '๐Ÿ“–', keywords: ['read', 'open'] },
+  { shortcode: 'books', emoji: '๐Ÿ“š', keywords: ['library', 'study'] },
+  { shortcode: 'newspaper', emoji: '๐Ÿ“ฐ', keywords: ['news', 'article'] },
+  { shortcode: 'calendar', emoji: '๐Ÿ“…', keywords: ['date', 'schedule'] },
+  { shortcode: 'chart', emoji: '๐Ÿ“ˆ', keywords: ['graph', 'increase'] },
+  { shortcode: 'chart_down', emoji: '๐Ÿ“‰', keywords: ['graph', 'decrease'] },
+  { shortcode: 'bar_chart', emoji: '๐Ÿ“Š', keywords: ['graph', 'stats'] },
+  { shortcode: 'clipboard', emoji: '๐Ÿ“‹', keywords: ['list', 'todo'] },
+  { shortcode: 'pushpin', emoji: '๐Ÿ“Œ', keywords: ['pin', 'location'] },
+  { shortcode: 'round_pushpin', emoji: '๐Ÿ“', keywords: ['pin', 'location'] },
+  { shortcode: 'triangular_ruler', emoji: '๐Ÿ“', keywords: ['math', 'measure'] },
+  { shortcode: 'straight_ruler', emoji: '๐Ÿ“', keywords: ['math', 'measure'] },
+  { shortcode: 'pencil', emoji: 'โœ๏ธ', keywords: ['write', 'draw'] },
+  { shortcode: 'pen', emoji: '๐Ÿ–Š๏ธ', keywords: ['write', 'sign'] },
+  { shortcode: 'crayon', emoji: '๐Ÿ–๏ธ', keywords: ['draw', 'color'] },
+  { shortcode: 'paintbrush', emoji: '๐Ÿ–Œ๏ธ', keywords: ['art', 'paint'] },
+  { shortcode: 'folder', emoji: '๐Ÿ“', keywords: ['file', 'directory'] },
+  { shortcode: 'open_folder', emoji: '๐Ÿ“‚', keywords: ['file', 'directory'] },
+
+  // Nature & Animals
+  { shortcode: 'dog', emoji: '๐Ÿถ', keywords: ['puppy', 'pet', 'woof'] },
+  { shortcode: 'cat_face', emoji: '๐Ÿฑ', keywords: ['kitty', 'pet', 'meow'] },
+  { shortcode: 'mouse', emoji: '๐Ÿญ', keywords: ['rodent'] },
+  { shortcode: 'hamster', emoji: '๐Ÿน', keywords: ['pet', 'rodent'] },
+  { shortcode: 'rabbit', emoji: '๐Ÿฐ', keywords: ['bunny', 'pet'] },
+  { shortcode: 'fox', emoji: '๐ŸฆŠ', keywords: ['animal'] },
+  { shortcode: 'bear', emoji: '๐Ÿป', keywords: ['animal'] },
+  { shortcode: 'panda', emoji: '๐Ÿผ', keywords: ['animal', 'cute'] },
+  { shortcode: 'koala', emoji: '๐Ÿจ', keywords: ['animal', 'australia'] },
+  { shortcode: 'tiger', emoji: '๐Ÿฏ', keywords: ['animal', 'cat'] },
+  { shortcode: 'lion', emoji: '๐Ÿฆ', keywords: ['animal', 'king'] },
+  { shortcode: 'cow', emoji: '๐Ÿฎ', keywords: ['animal', 'farm'] },
+  { shortcode: 'pig', emoji: '๐Ÿท', keywords: ['animal', 'farm'] },
+  { shortcode: 'frog', emoji: '๐Ÿธ', keywords: ['animal', 'toad'] },
+  { shortcode: 'monkey_face', emoji: '๐Ÿต', keywords: ['animal', 'ape'] },
+  { shortcode: 'chicken', emoji: '๐Ÿ”', keywords: ['animal', 'farm', 'hen'] },
+  { shortcode: 'penguin', emoji: '๐Ÿง', keywords: ['animal', 'bird'] },
+  { shortcode: 'bird', emoji: '๐Ÿฆ', keywords: ['animal', 'fly'] },
+  { shortcode: 'eagle', emoji: '๐Ÿฆ…', keywords: ['animal', 'bird'] },
+  { shortcode: 'duck', emoji: '๐Ÿฆ†', keywords: ['animal', 'bird', 'quack'] },
+  { shortcode: 'owl', emoji: '๐Ÿฆ‰', keywords: ['animal', 'bird', 'night'] },
+  { shortcode: 'bat', emoji: '๐Ÿฆ‡', keywords: ['animal', 'night', 'vampire'] },
+  { shortcode: 'wolf', emoji: '๐Ÿบ', keywords: ['animal'] },
+  { shortcode: 'horse', emoji: '๐Ÿด', keywords: ['animal'] },
+  { shortcode: 'unicorn', emoji: '๐Ÿฆ„', keywords: ['animal', 'magic'] },
+  { shortcode: 'bee', emoji: '๐Ÿ', keywords: ['insect', 'honey'] },
+  { shortcode: 'bug', emoji: '๐Ÿ›', keywords: ['insect', 'caterpillar'] },
+  { shortcode: 'butterfly', emoji: '๐Ÿฆ‹', keywords: ['insect', 'pretty'] },
+  { shortcode: 'snail', emoji: '๐ŸŒ', keywords: ['slow'] },
+  { shortcode: 'lady_beetle', emoji: '๐Ÿž', keywords: ['insect', 'bug'] },
+  { shortcode: 'ant', emoji: '๐Ÿœ', keywords: ['insect', 'bug'] },
+  { shortcode: 'spider', emoji: '๐Ÿ•ท๏ธ', keywords: ['insect', 'scary'] },
+  { shortcode: 'turtle', emoji: '๐Ÿข', keywords: ['animal', 'slow'] },
+  { shortcode: 'snake', emoji: '๐Ÿ', keywords: ['animal', 'reptile'] },
+  { shortcode: 'dragon', emoji: '๐Ÿฒ', keywords: ['animal', 'mythical'] },
+  { shortcode: 'dinosaur', emoji: '๐Ÿฆ•', keywords: ['animal', 'extinct'] },
+  { shortcode: 't_rex', emoji: '๐Ÿฆ–', keywords: ['animal', 'dinosaur'] },
+  { shortcode: 'whale', emoji: '๐Ÿณ', keywords: ['animal', 'ocean'] },
+  { shortcode: 'dolphin', emoji: '๐Ÿฌ', keywords: ['animal', 'ocean'] },
+  { shortcode: 'fish', emoji: '๐ŸŸ', keywords: ['animal', 'ocean'] },
+  { shortcode: 'tropical_fish', emoji: '๐Ÿ ', keywords: ['animal', 'ocean'] },
+  { shortcode: 'shark', emoji: '๐Ÿฆˆ', keywords: ['animal', 'ocean'] },
+  { shortcode: 'octopus', emoji: '๐Ÿ™', keywords: ['animal', 'ocean'] },
+  { shortcode: 'crab', emoji: '๐Ÿฆ€', keywords: ['animal', 'ocean'] },
+  { shortcode: 'lobster', emoji: '๐Ÿฆž', keywords: ['animal', 'ocean'] },
+  { shortcode: 'shrimp', emoji: '๐Ÿฆ', keywords: ['animal', 'ocean'] },
+
+  // Plants & Nature
+  { shortcode: 'bouquet', emoji: '๐Ÿ’', keywords: ['flowers', 'gift'] },
+  { shortcode: 'cherry_blossom', emoji: '๐ŸŒธ', keywords: ['flower', 'spring'] },
+  { shortcode: 'rose', emoji: '๐ŸŒน', keywords: ['flower', 'love'] },
+  { shortcode: 'tulip', emoji: '๐ŸŒท', keywords: ['flower', 'spring'] },
+  { shortcode: 'sunflower', emoji: '๐ŸŒป', keywords: ['flower', 'summer'] },
+  { shortcode: 'hibiscus', emoji: '๐ŸŒบ', keywords: ['flower', 'tropical'] },
+  { shortcode: 'seedling', emoji: '๐ŸŒฑ', keywords: ['plant', 'grow'] },
+  { shortcode: 'evergreen_tree', emoji: '๐ŸŒฒ', keywords: ['tree', 'pine'] },
+  { shortcode: 'deciduous_tree', emoji: '๐ŸŒณ', keywords: ['tree'] },
+  { shortcode: 'palm_tree', emoji: '๐ŸŒด', keywords: ['tree', 'tropical'] },
+  { shortcode: 'cactus', emoji: '๐ŸŒต', keywords: ['plant', 'desert'] },
+  { shortcode: 'herb', emoji: '๐ŸŒฟ', keywords: ['plant', 'leaf'] },
+  { shortcode: 'shamrock', emoji: 'โ˜˜๏ธ', keywords: ['clover', 'irish'] },
+  { shortcode: 'four_leaf_clover', emoji: '๐Ÿ€', keywords: ['luck', 'irish'] },
+  { shortcode: 'maple_leaf', emoji: '๐Ÿ', keywords: ['fall', 'autumn'] },
+  { shortcode: 'fallen_leaf', emoji: '๐Ÿ‚', keywords: ['fall', 'autumn'] },
+  { shortcode: 'leaves', emoji: '๐Ÿƒ', keywords: ['leaf', 'wind'] },
+  { shortcode: 'mushroom', emoji: '๐Ÿ„', keywords: ['fungus'] },
+
+  // Food & Drink
+  { shortcode: 'apple', emoji: '๐ŸŽ', keywords: ['fruit', 'red'] },
+  { shortcode: 'green_apple', emoji: '๐Ÿ', keywords: ['fruit'] },
+  { shortcode: 'pear', emoji: '๐Ÿ', keywords: ['fruit'] },
+  { shortcode: 'orange', emoji: '๐ŸŠ', keywords: ['fruit', 'citrus'] },
+  { shortcode: 'lemon', emoji: '๐Ÿ‹', keywords: ['fruit', 'citrus'] },
+  { shortcode: 'banana', emoji: '๐ŸŒ', keywords: ['fruit'] },
+  { shortcode: 'watermelon', emoji: '๐Ÿ‰', keywords: ['fruit', 'summer'] },
+  { shortcode: 'grapes', emoji: '๐Ÿ‡', keywords: ['fruit', 'wine'] },
+  { shortcode: 'strawberry', emoji: '๐Ÿ“', keywords: ['fruit', 'berry'] },
+  { shortcode: 'cherries', emoji: '๐Ÿ’', keywords: ['fruit'] },
+  { shortcode: 'peach', emoji: '๐Ÿ‘', keywords: ['fruit'] },
+  { shortcode: 'mango', emoji: '๐Ÿฅญ', keywords: ['fruit', 'tropical'] },
+  { shortcode: 'pineapple', emoji: '๐Ÿ', keywords: ['fruit', 'tropical'] },
+  { shortcode: 'coconut', emoji: '๐Ÿฅฅ', keywords: ['fruit', 'tropical'] },
+  { shortcode: 'avocado', emoji: '๐Ÿฅ‘', keywords: ['fruit', 'guacamole'] },
+  { shortcode: 'tomato', emoji: '๐Ÿ…', keywords: ['vegetable', 'red'] },
+  { shortcode: 'eggplant', emoji: '๐Ÿ†', keywords: ['vegetable', 'purple'] },
+  { shortcode: 'potato', emoji: '๐Ÿฅ”', keywords: ['vegetable', 'spud'] },
+  { shortcode: 'carrot', emoji: '๐Ÿฅ•', keywords: ['vegetable', 'orange'] },
+  { shortcode: 'corn', emoji: '๐ŸŒฝ', keywords: ['vegetable', 'maize'] },
+  { shortcode: 'hot_pepper', emoji: '๐ŸŒถ๏ธ', keywords: ['spicy', 'chili'] },
+  { shortcode: 'broccoli', emoji: '๐Ÿฅฆ', keywords: ['vegetable', 'green'] },
+  { shortcode: 'bread', emoji: '๐Ÿž', keywords: ['food', 'toast'] },
+  { shortcode: 'croissant', emoji: '๐Ÿฅ', keywords: ['food', 'french'] },
+  { shortcode: 'pretzel', emoji: '๐Ÿฅจ', keywords: ['food', 'snack'] },
+  { shortcode: 'bagel', emoji: '๐Ÿฅฏ', keywords: ['food', 'breakfast'] },
+  { shortcode: 'cheese', emoji: '๐Ÿง€', keywords: ['food', 'dairy'] },
+  { shortcode: 'egg', emoji: '๐Ÿฅš', keywords: ['food', 'breakfast'] },
+  { shortcode: 'bacon', emoji: '๐Ÿฅ“', keywords: ['food', 'breakfast'] },
+  { shortcode: 'pancakes', emoji: '๐Ÿฅž', keywords: ['food', 'breakfast'] },
+  { shortcode: 'waffle', emoji: '๐Ÿง‡', keywords: ['food', 'breakfast'] },
+  { shortcode: 'steak', emoji: '๐Ÿฅฉ', keywords: ['food', 'meat'] },
+  { shortcode: 'poultry_leg', emoji: '๐Ÿ—', keywords: ['food', 'chicken'] },
+  { shortcode: 'hamburger', emoji: '๐Ÿ”', keywords: ['food', 'burger'] },
+  { shortcode: 'fries', emoji: '๐ŸŸ', keywords: ['food', 'fast'] },
+  { shortcode: 'pizza', emoji: '๐Ÿ•', keywords: ['food', 'italian'] },
+  { shortcode: 'hot_dog', emoji: '๐ŸŒญ', keywords: ['food', 'fast'] },
+  { shortcode: 'sandwich', emoji: '๐Ÿฅช', keywords: ['food', 'lunch'] },
+  { shortcode: 'taco', emoji: '๐ŸŒฎ', keywords: ['food', 'mexican'] },
+  { shortcode: 'burrito', emoji: '๐ŸŒฏ', keywords: ['food', 'mexican'] },
+  { shortcode: 'sushi', emoji: '๐Ÿฃ', keywords: ['food', 'japanese'] },
+  { shortcode: 'ramen', emoji: '๐Ÿœ', keywords: ['food', 'noodles'] },
+  { shortcode: 'spaghetti', emoji: '๐Ÿ', keywords: ['food', 'pasta'] },
+  { shortcode: 'curry', emoji: '๐Ÿ›', keywords: ['food', 'rice'] },
+  { shortcode: 'rice', emoji: '๐Ÿš', keywords: ['food', 'white'] },
+  { shortcode: 'salad', emoji: '๐Ÿฅ—', keywords: ['food', 'healthy'] },
+  { shortcode: 'popcorn', emoji: '๐Ÿฟ', keywords: ['food', 'movie'] },
+  { shortcode: 'cake', emoji: '๐ŸŽ‚', keywords: ['food', 'birthday'] },
+  { shortcode: 'cupcake', emoji: '๐Ÿง', keywords: ['food', 'sweet'] },
+  { shortcode: 'pie', emoji: '๐Ÿฅง', keywords: ['food', 'dessert'] },
+  { shortcode: 'cookie', emoji: '๐Ÿช', keywords: ['food', 'sweet'] },
+  { shortcode: 'chocolate', emoji: '๐Ÿซ', keywords: ['food', 'sweet'] },
+  { shortcode: 'candy', emoji: '๐Ÿฌ', keywords: ['food', 'sweet'] },
+  { shortcode: 'lollipop', emoji: '๐Ÿญ', keywords: ['food', 'sweet'] },
+  { shortcode: 'donut', emoji: '๐Ÿฉ', keywords: ['food', 'sweet'] },
+  { shortcode: 'ice_cream', emoji: '๐Ÿจ', keywords: ['food', 'dessert'] },
+  { shortcode: 'icecream', emoji: '๐Ÿฆ', keywords: ['food', 'dessert', 'cone'] },
+  { shortcode: 'coffee', emoji: 'โ˜•', keywords: ['drink', 'caffeine'] },
+  { shortcode: 'tea', emoji: '๐Ÿต', keywords: ['drink', 'green'] },
+  { shortcode: 'beer', emoji: '๐Ÿบ', keywords: ['drink', 'alcohol'] },
+  { shortcode: 'beers', emoji: '๐Ÿป', keywords: ['drink', 'cheers'] },
+  { shortcode: 'wine_glass', emoji: '๐Ÿท', keywords: ['drink', 'alcohol'] },
+  { shortcode: 'cocktail', emoji: '๐Ÿธ', keywords: ['drink', 'alcohol'] },
+  { shortcode: 'tropical_drink', emoji: '๐Ÿน', keywords: ['drink', 'vacation'] },
+  { shortcode: 'champagne', emoji: '๐Ÿพ', keywords: ['drink', 'celebrate'] },
+  { shortcode: 'milk', emoji: '๐Ÿฅ›', keywords: ['drink', 'dairy'] },
+  { shortcode: 'baby_bottle', emoji: '๐Ÿผ', keywords: ['drink', 'infant'] },
+  { shortcode: 'juice', emoji: '๐Ÿงƒ', keywords: ['drink', 'box'] },
+  { shortcode: 'cup_with_straw', emoji: '๐Ÿฅค', keywords: ['drink', 'soda'] },
+
+  // Weather & Nature
+  { shortcode: 'sun', emoji: 'โ˜€๏ธ', keywords: ['weather', 'sunny', 'bright'] },
+  { shortcode: 'moon', emoji: '๐ŸŒ™', keywords: ['night', 'sleep'] },
+  { shortcode: 'full_moon', emoji: '๐ŸŒ•', keywords: ['night', 'lunar'] },
+  { shortcode: 'new_moon', emoji: '๐ŸŒ‘', keywords: ['night', 'dark'] },
+  { shortcode: 'star2', emoji: '๐ŸŒŸ', keywords: ['glow', 'sparkle'] },
+  { shortcode: 'milky_way', emoji: '๐ŸŒŒ', keywords: ['galaxy', 'space'] },
+  { shortcode: 'cloud', emoji: 'โ˜๏ธ', keywords: ['weather', 'sky'] },
+  { shortcode: 'sun_behind_cloud', emoji: 'โ›…', keywords: ['weather'] },
+  { shortcode: 'cloud_with_rain', emoji: '๐ŸŒง๏ธ', keywords: ['weather', 'rainy'] 
},
+  { shortcode: 'thunder', emoji: 'โ›ˆ๏ธ', keywords: ['weather', 'storm'] },
+  { shortcode: 'snowflake', emoji: 'โ„๏ธ', keywords: ['weather', 'cold'] },
+  { shortcode: 'snowman', emoji: 'โ˜ƒ๏ธ', keywords: ['winter', 'snow'] },
+  { shortcode: 'wind_blowing', emoji: '๐ŸŒฌ๏ธ', keywords: ['weather', 'air'] },
+  { shortcode: 'tornado', emoji: '๐ŸŒช๏ธ', keywords: ['weather', 'storm'] },
+  { shortcode: 'fog', emoji: '๐ŸŒซ๏ธ', keywords: ['weather', 'mist'] },
+  { shortcode: 'umbrella', emoji: 'โ˜‚๏ธ', keywords: ['rain', 'weather'] },
+  { shortcode: 'rainbow', emoji: '๐ŸŒˆ', keywords: ['weather', 'pride'] },
+  { shortcode: 'earth', emoji: '๐ŸŒ', keywords: ['world', 'planet'] },
+  { shortcode: 'earth_americas', emoji: '๐ŸŒŽ', keywords: ['world', 'planet'] },
+  { shortcode: 'earth_asia', emoji: '๐ŸŒ', keywords: ['world', 'planet'] },
+  { shortcode: 'rocket', emoji: '๐Ÿš€', keywords: ['space', 'launch'] },
+  { shortcode: 'satellite', emoji: '๐Ÿ›ฐ๏ธ', keywords: ['space', 'orbit'] },
+  { shortcode: 'ufo', emoji: '๐Ÿ›ธ', keywords: ['alien', 'space'] },
+
+  // Checkmarks & Common Symbols
+  { shortcode: 'white_check_mark', emoji: 'โœ…', keywords: ['done', 'yes', 'ok'] 
},
+  { shortcode: 'check', emoji: 'โœ”๏ธ', keywords: ['done', 'yes'] },
+  { shortcode: 'x', emoji: 'โŒ', keywords: ['no', 'wrong', 'cancel'] },
+  { shortcode: 'cross_mark', emoji: 'โŽ', keywords: ['no', 'wrong'] },
+  { shortcode: 'plus', emoji: 'โž•', keywords: ['add', 'math'] },
+  { shortcode: 'minus', emoji: 'โž–', keywords: ['subtract', 'math'] },
+  { shortcode: 'divide', emoji: 'โž—', keywords: ['math', 'division'] },
+  { shortcode: 'multiply', emoji: 'โœ–๏ธ', keywords: ['math', 'times'] },
+  { shortcode: 'infinity', emoji: 'โ™พ๏ธ', keywords: ['forever', 'endless'] },
+  { shortcode: 'question', emoji: 'โ“', keywords: ['ask', 'what'] },
+  { shortcode: 'grey_question', emoji: 'โ”', keywords: ['ask', 'what'] },
+  { shortcode: 'exclamation', emoji: 'โ—', keywords: ['alert', 'important'] },
+  { shortcode: 'grey_exclamation', emoji: 'โ•', keywords: ['alert'] },
+  { shortcode: 'warning', emoji: 'โš ๏ธ', keywords: ['alert', 'caution'] },
+  { shortcode: 'no_entry', emoji: 'โ›”', keywords: ['stop', 'forbidden'] },
+  { shortcode: 'prohibited', emoji: '๐Ÿšซ', keywords: ['stop', 'banned'] },
+  { shortcode: 'recycle', emoji: 'โ™ป๏ธ', keywords: ['environment', 'green'] },
+  { shortcode: 'arrow_up', emoji: 'โฌ†๏ธ', keywords: ['direction', 'north'] },
+  { shortcode: 'arrow_down', emoji: 'โฌ‡๏ธ', keywords: ['direction', 'south'] },
+  { shortcode: 'arrow_left', emoji: 'โฌ…๏ธ', keywords: ['direction', 'west'] },
+  { shortcode: 'arrow_right', emoji: 'โžก๏ธ', keywords: ['direction', 'east'] },
+  {
+    shortcode: 'arrow_upper_right',
+    emoji: 'โ†—๏ธ',
+    keywords: ['direction', 'northeast'],
+  },
+  {
+    shortcode: 'arrow_lower_right',
+    emoji: 'โ†˜๏ธ',
+    keywords: ['direction', 'southeast'],
+  },
+  {
+    shortcode: 'arrow_lower_left',
+    emoji: 'โ†™๏ธ',
+    keywords: ['direction', 'southwest'],
+  },
+  {
+    shortcode: 'arrow_upper_left',
+    emoji: 'โ†–๏ธ',
+    keywords: ['direction', 'northwest'],
+  },
+  {
+    shortcode: 'left_right_arrow',
+    emoji: 'โ†”๏ธ',
+    keywords: ['direction', 'horizontal'],
+  },
+  {
+    shortcode: 'up_down_arrow',
+    emoji: 'โ†•๏ธ',
+    keywords: ['direction', 'vertical'],
+  },
+  { shortcode: 'arrows_clockwise', emoji: '๐Ÿ”ƒ', keywords: ['refresh', 'sync'] },
+  {
+    shortcode: 'arrows_counterclockwise',
+    emoji: '๐Ÿ”„',
+    keywords: ['refresh', 'sync'],
+  },
+  { shortcode: 'back', emoji: '๐Ÿ”™', keywords: ['return', 'previous'] },
+  { shortcode: 'end', emoji: '๐Ÿ”š', keywords: ['finish', 'last'] },
+  { shortcode: 'on', emoji: '๐Ÿ”›', keywords: ['active'] },
+  { shortcode: 'soon', emoji: '๐Ÿ”œ', keywords: ['coming', 'future'] },
+  { shortcode: 'top', emoji: '๐Ÿ”', keywords: ['best', 'first'] },
+  { shortcode: 'new', emoji: '๐Ÿ†•', keywords: ['fresh', 'latest'] },
+  { shortcode: 'free', emoji: '๐Ÿ†“', keywords: ['gratis', 'cost'] },
+  { shortcode: 'up', emoji: '๐Ÿ†™', keywords: ['increase', 'level'] },
+  { shortcode: 'cool', emoji: '๐Ÿ†’', keywords: ['nice', 'awesome'] },
+  { shortcode: 'ok', emoji: '๐Ÿ†—', keywords: ['yes', 'approve'] },
+  { shortcode: 'sos', emoji: '๐Ÿ†˜', keywords: ['help', 'emergency'] },
+  { shortcode: 'stop_sign', emoji: '๐Ÿ›‘', keywords: ['halt', 'cease'] },
+  { shortcode: 'a', emoji: '๐Ÿ…ฐ๏ธ', keywords: ['letter', 'blood'] },
+  { shortcode: 'b', emoji: '๐Ÿ…ฑ๏ธ', keywords: ['letter', 'blood'] },
+  { shortcode: 'o', emoji: '๐Ÿ…พ๏ธ', keywords: ['letter', 'blood'] },
+  { shortcode: 'information', emoji: 'โ„น๏ธ', keywords: ['info', 'help'] },
+  { shortcode: 'copyright', emoji: 'ยฉ๏ธ', keywords: ['legal', 'ip'] },
+  { shortcode: 'registered', emoji: 'ยฎ๏ธ', keywords: ['legal', 'brand'] },
+  { shortcode: 'tm', emoji: 'โ„ข๏ธ', keywords: ['legal', 'trademark'] },
+  { shortcode: 'one', emoji: '1๏ธโƒฃ', keywords: ['number', 'first'] },
+  { shortcode: 'two', emoji: '2๏ธโƒฃ', keywords: ['number', 'second'] },
+  { shortcode: 'three', emoji: '3๏ธโƒฃ', keywords: ['number', 'third'] },
+  { shortcode: 'four', emoji: '4๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'five', emoji: '5๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'six', emoji: '6๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'seven', emoji: '7๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'eight', emoji: '8๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'nine', emoji: '9๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'zero', emoji: '0๏ธโƒฃ', keywords: ['number'] },
+  { shortcode: 'keycap_ten', emoji: '๐Ÿ”Ÿ', keywords: ['number', 'ten'] },
+  { shortcode: 'hash', emoji: '#๏ธโƒฃ', keywords: ['number', 'pound', 'hashtag'] 
},
+  { shortcode: 'asterisk', emoji: '*๏ธโƒฃ', keywords: ['star', 'symbol'] },
+  { shortcode: 'eject', emoji: 'โ๏ธ', keywords: ['media', 'remove'] },
+  { shortcode: 'play', emoji: 'โ–ถ๏ธ', keywords: ['media', 'start'] },
+  { shortcode: 'pause', emoji: 'โธ๏ธ', keywords: ['media', 'wait'] },
+  { shortcode: 'stop', emoji: 'โน๏ธ', keywords: ['media', 'end'] },
+  { shortcode: 'record', emoji: 'โบ๏ธ', keywords: ['media', 'red'] },
+  { shortcode: 'fast_forward', emoji: 'โฉ', keywords: ['media', 'skip'] },
+  { shortcode: 'rewind', emoji: 'โช', keywords: ['media', 'back'] },
+  { shortcode: 'next_track', emoji: 'โญ๏ธ', keywords: ['media', 'skip'] },
+  { shortcode: 'previous_track', emoji: 'โฎ๏ธ', keywords: ['media', 'back'] },
+  { shortcode: 'cinema', emoji: '๐ŸŽฆ', keywords: ['movie', 'film'] },
+  { shortcode: 'low_brightness', emoji: '๐Ÿ”…', keywords: ['dim', 'light'] },
+  { shortcode: 'high_brightness', emoji: '๐Ÿ”†', keywords: ['bright', 'light'] },
+  { shortcode: 'signal_strength', emoji: '๐Ÿ“ถ', keywords: ['wifi', 'bars'] },
+  { shortcode: 'vibration', emoji: '๐Ÿ“ณ', keywords: ['phone', 'mode'] },
+  { shortcode: 'mobile_off', emoji: '๐Ÿ“ด', keywords: ['phone', 'silent'] },
+  { shortcode: 'female', emoji: 'โ™€๏ธ', keywords: ['woman', 'gender'] },
+  { shortcode: 'male', emoji: 'โ™‚๏ธ', keywords: ['man', 'gender'] },
+  { shortcode: 'medical', emoji: 'โš•๏ธ', keywords: ['health', 'doctor'] },
+  { shortcode: 'atom', emoji: 'โš›๏ธ', keywords: ['science', 'physics'] },
+];
+
+/**
+ * Filter emojis by search text (checks shortcode and keywords)
+ */
+export function filterEmojis(
+  searchText: string,
+  limit: number = 10,
+): EmojiItem[] {
+  if (!searchText) return [];
+
+  const lowerSearch = searchText.toLowerCase();
+  return EMOJI_DATA.filter(
+    item =>
+      item.shortcode.toLowerCase().includes(lowerSearch) ||
+      item.keywords?.some(keyword =>
+        keyword.toLowerCase().includes(lowerSearch),
+      ),
+  ).slice(0, limit);
+}
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx
new file mode 100644
index 0000000000..929fb69bca
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx
@@ -0,0 +1,247 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { forwardRef, useCallback, useMemo, useState, useRef } from 'react';
+import { Mentions } from 'antd';
+import type { MentionsRef, MentionsProps } from 'antd/es/mentions';
+import { filterEmojis, type EmojiItem } from './emojiData';
+
+const MIN_CHARS_BEFORE_POPUP = 2;
+
+// Regex to match emoji characters (simplified, covers most common emojis)
+const EMOJI_REGEX =
+  
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u;
+
+export interface EmojiTextAreaProps
+  extends Omit<MentionsProps, 'prefix' | 'options' | 'onSelect'> {
+  /**
+   * Minimum characters after colon before showing popup.
+   * @default 2 (Slack-like behavior)
+   */
+  minCharsBeforePopup?: number;
+  /**
+   * Maximum number of emoji suggestions to show.
+   * @default 10
+   */
+  maxSuggestions?: number;
+  /**
+   * Called when an emoji is selected from the popup.
+   */
+  onEmojiSelect?: (emoji: EmojiItem) => void;
+}
+
+/**
+ * A TextArea component with Slack-like emoji autocomplete.
+ *
+ * Features:
+ * - Triggers on `:` prefix (like Slack)
+ * - Only shows popup after 2+ characters are typed (configurable)
+ * - Colon must be preceded by a space, start of line, or another emoji
+ * - Prevents accidental Enter key selection when typing quickly
+ *
+ * @example
+ * ```tsx
+ * <EmojiTextArea
+ *   placeholder="Type :sm to see emoji suggestions..."
+ *   onChange={(text) => console.log(text)}
+ * />
+ * ```
+ */
+export const EmojiTextArea = forwardRef<MentionsRef, EmojiTextAreaProps>(
+  (
+    {
+      minCharsBeforePopup = MIN_CHARS_BEFORE_POPUP,
+      maxSuggestions = 10,
+      onEmojiSelect,
+      onChange,
+      onKeyDown,
+      ...restProps
+    },
+    ref,
+  ) => {
+    const [options, setOptions] = useState<
+      Array<{ value: string; label: React.ReactNode }>
+    >([]);
+    const [isPopupVisible, setIsPopupVisible] = useState(false);
+    const lastSearchRef = useRef<string>('');
+    const lastKeyPressTimeRef = useRef<number>(0);
+
+    /**
+     * Validates whether the colon trigger should activate the popup.
+     * Implements Slack-like behavior:
+     * - Colon must be preceded by whitespace, start of text, or emoji
+     * - At least minCharsBeforePopup characters must be typed after colon
+     */
+    const validateSearch = useCallback(
+      (text: string, props: MentionsProps): boolean => {
+        // Get the full value to check what precedes the colon
+        const fullValue = (props.value as string) || '';
+
+        // Find where this search text starts in the full value
+        // The search text is what comes after the `:` prefix
+        const colonIndex = fullValue.lastIndexOf(`:${text}`);
+
+        if (colonIndex === -1) {
+          setIsPopupVisible(false);
+          return false;
+        }
+
+        // Check what precedes the colon
+        if (colonIndex > 0) {
+          const charBefore = fullValue[colonIndex - 1];
+
+          // Must be preceded by whitespace, newline, or emoji
+          const isWhitespace = /\s/.test(charBefore);
+          const isEmoji = EMOJI_REGEX.test(charBefore);
+
+          if (!isWhitespace && !isEmoji) {
+            setIsPopupVisible(false);
+            return false;
+          }
+        }
+
+        // Check minimum character requirement
+        if (text.length < minCharsBeforePopup) {
+          setIsPopupVisible(false);
+          return false;
+        }
+
+        setIsPopupVisible(true);
+        return true;
+      },
+      [minCharsBeforePopup],
+    );
+
+    /**
+     * Handles search and filters emoji suggestions.
+     */
+    const handleSearch = useCallback(
+      (searchText: string) => {
+        lastSearchRef.current = searchText;
+
+        if (searchText.length < minCharsBeforePopup) {
+          setOptions([]);
+          return;
+        }
+
+        const filteredEmojis = filterEmojis(searchText, maxSuggestions);
+
+        const newOptions = filteredEmojis.map(item => ({
+          value: item.emoji,
+          label: (
+            <span>
+              <span style={{ marginRight: 8 }}>{item.emoji}</span>
+              <span style={{ color: 'var(--ant-color-text-secondary)' }}>
+                :{item.shortcode}:
+              </span>
+            </span>
+          ),
+          // Store the full item for onSelect callback
+          data: item,
+        }));
+
+        setOptions(newOptions);
+      },
+      [minCharsBeforePopup, maxSuggestions],
+    );
+
+    /**
+     * Handles emoji selection from the popup.
+     */
+    const handleSelect = useCallback(
+      (option: { value: string; data?: EmojiItem }) => {
+        if (option.data && onEmojiSelect) {
+          onEmojiSelect(option.data);
+        }
+        setIsPopupVisible(false);
+      },
+      [onEmojiSelect],
+    );
+
+    /**
+     * Handles key down events to prevent accidental selection on Enter.
+     * If the user presses Enter very quickly after typing (< 100ms),
+     * we treat it as a newline intent rather than selection.
+     */
+    const handleKeyDown = useCallback(
+      (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+        const now = Date.now();
+        const timeSinceLastKey = now - lastKeyPressTimeRef.current;
+
+        // If Enter is pressed and popup is visible
+        if (e.key === 'Enter' && isPopupVisible) {
+          // If typed very quickly (< 100ms since last keypress) and
+          // there's meaningful search text, allow the Enter to create newline
+          // This prevents accidental selection when typing something like:
+          // "let me show you an example:[Enter]"
+          if (timeSinceLastKey < 100 && lastSearchRef.current.length === 0) {
+            // Let the default behavior (newline) happen
+            setIsPopupVisible(false);
+            return;
+          }
+        }
+
+        lastKeyPressTimeRef.current = now;
+
+        // Call original onKeyDown if provided
+        onKeyDown?.(e);
+      },
+      [isPopupVisible, onKeyDown],
+    );
+
+    const handleChange = useCallback(
+      (text: string) => {
+        lastKeyPressTimeRef.current = Date.now();
+        onChange?.(text);
+      },
+      [onChange],
+    );
+
+    // Memoize the Mentions component props
+    const mentionsProps = useMemo(
+      () => ({
+        prefix: ':',
+        split: '',
+        options,
+        validateSearch,
+        onSearch: handleSearch,
+        onSelect: handleSelect,
+        onKeyDown: handleKeyDown,
+        onChange: handleChange,
+        notFoundContent: null, // Don't show "Not Found" message
+        ...restProps,
+      }),
+      [
+        options,
+        validateSearch,
+        handleSearch,
+        handleSelect,
+        handleKeyDown,
+        handleChange,
+        restProps,
+      ],
+    );
+
+    return <Mentions ref={ref} {...mentionsProps} />;
+  },
+);
+
+EmojiTextArea.displayName = 'EmojiTextArea';
+
+export type { EmojiItem };
+export { filterEmojis, EMOJI_DATA } from './emojiData';
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/index.ts 
b/superset-frontend/packages/superset-ui-core/src/components/index.ts
index 12a0504ce5..8dfc63f9e6 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts
@@ -102,6 +102,13 @@ export {
   type DynamicEditableTitleProps,
 } from './DynamicEditableTitle';
 export { EditableTitle, type EditableTitleProps } from './EditableTitle';
+export {
+  EmojiTextArea,
+  type EmojiTextAreaProps,
+  type EmojiItem,
+  filterEmojis,
+  EMOJI_DATA,
+} from './EmojiTextArea';
 export { EmptyState, type EmptyStateProps } from './EmptyState';
 export { Empty, type EmptyProps } from './EmptyState/Empty';
 export { FaveStar, type FaveStarProps } from './FaveStar';

Reply via email to