{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-emoji-picker-3","type":"registry:block","title":"Message reactions","description":"A product update with reactions you can toggle, a tooltip naming who reacted and a picker that opens on quick reactions.","dependencies":[],"registryDependencies":["avatar","button","@reui/emoji-picker","item","tooltip"],"files":[{"path":"c-emoji-picker-3.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useId, useRef, useState } from \"react\"\nimport {\n  DEFAULT_EMOJI_PICKER_I18N,\n  EmojiPicker,\n  EmojiPickerCategories,\n  EmojiPickerContent,\n  EmojiPickerFooter,\n  EmojiPickerHeader,\n  EmojiPickerList,\n  EmojiPickerPreview,\n  EmojiPickerSearch,\n  EmojiPickerSkinTone,\n  EmojiPickerTrigger,\n  EmojiPickerValue,\n  findEmoji,\n  getEmojiValue,\n  useEmojiPicker,\n  type EmojiPickerEmoji,\n  type EmojiPickerSkinTone as SkinTone,\n} from \"@/components/reui/emoji-picker\"\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Item,\n  ItemContent,\n  ItemMedia,\n  ItemTitle,\n} from \"@/components/ui/item\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ntype Reaction = {\n  /** The toned value, so each skin tone gets a pill of its own. */\n  emoji: string\n  label: string\n  /** Everyone else who reacted, in the order they did. */\n  people: string[]\n  mine: boolean\n}\n\nconst INITIAL_REACTIONS: Reaction[] = [\n  {\n    emoji: \"🎉\",\n    label: \"party popper\",\n    people: [\"Amara Okafor\", \"Priya Shah\"],\n    mine: true,\n  },\n  {\n    emoji: \"🚀\",\n    label: \"rocket\",\n    people: [\"Daniel Kim\", \"Michael Rodriguez\", \"Priya Shah\"],\n    mine: false,\n  },\n  {\n    emoji: \"👍🏽\",\n    label: \"thumbs up, medium skin tone\",\n    people: [\"Michael Rodriguez\", \"Amara Okafor\"],\n    mine: false,\n  },\n  { emoji: \"👀\", label: \"eyes\", people: [\"Daniel Kim\"], mine: false },\n]\n\n/* Untoned on purpose: the row draws them in the tone picked in the footer,\n   so a toned 👍 joins the matching pill instead of starting a new one. */\nconst QUICK_REACTIONS = [\"👍\", \"❤️\", \"😂\", \"🎉\", \"🙏\", \"👀\"]\n\nconst TONES = DEFAULT_EMOJI_PICKER_I18N.labels.skinTones\n\n/* The tone goes into the name, or two pills of one emoji would announce\n   the same thing. */\nconst labelOf = (emoji: EmojiPickerEmoji, tone: SkinTone) =>\n  tone > 0 && emoji.skins\n    ? `${emoji.label}, ${TONES[tone].toLowerCase()} skin tone`\n    : emoji.label\n\nconst countOf = (reaction: Reaction) =>\n  reaction.people.length + (reaction.mine ? 1 : 0)\n\n/* \"Amara Okafor, Priya Shah and you\": you always come last. */\nfunction namesOf(reaction: Reaction) {\n  const names = reaction.mine ? [...reaction.people, \"you\"] : reaction.people\n  if (names.length === 1) return reaction.mine ? \"You\" : names[0]\n  return `${names.slice(0, -1).join(\", \")} and ${names[names.length - 1]}`\n}\n\n/* A composed first row in the popover, so the common reactions take one\n   click. It goes through the picker's own `select`, so the skin tone, the\n   recent list and closing on pick behave exactly like a grid pick. Columns\n   one cell wide from the grid's gutter put each glyph over a grid column. */\nfunction QuickReactions({ reactions }: { reactions: Reaction[] }) {\n  const { emojis, skinTone, select } = useEmojiPicker()\n  return (\n    <div\n      role=\"group\"\n      aria-label=\"Quick reactions\"\n      className=\"grid auto-cols-(--emoji-picker-cell) grid-flow-col justify-start justify-items-center px-(--emoji-picker-gutter) pt-(--emoji-picker-padding)\"\n    >\n      {QUICK_REACTIONS.map((glyph) => {\n        /* Undefined until the data arrives, which the trigger starts\n           fetching on hover, so the row is rarely disabled for long. */\n        const emoji = findEmoji(emojis, glyph)\n        const value = emoji ? getEmojiValue(emoji, skinTone) : glyph\n        const mine = reactions.some(\n          (reaction) => reaction.mine && reaction.emoji === value\n        )\n        return (\n          /* Pressed reads as a quiet fill, not a solid primary tile. */\n          <Button\n            key={glyph}\n            variant={mine ? \"secondary\" : \"ghost\"}\n            size=\"icon-sm\"\n            aria-pressed={mine}\n            aria-label={emoji ? labelOf(emoji, skinTone) : glyph}\n            disabled={!emoji}\n            onClick={() => emoji && select(emoji)}\n          >\n            <span\n              aria-hidden=\"true\"\n              className=\"text-(length:--emoji-picker-emoji) leading-none\"\n            >\n              <EmojiPickerValue value={value} />\n            </span>\n          </Button>\n        )\n      })}\n    </div>\n  )\n}\n\nexport function Pattern() {\n  const id = useId()\n  const [reactions, setReactions] = useState(INITIAL_REACTIONS)\n  const [announcement, setAnnouncement] = useState(\"\")\n  const triggerRef = useRef<HTMLButtonElement>(null)\n\n  /* One path for a pill, a quick reaction and a grid pick: reacting with\n     an emoji you already used takes yours back, and a pill nobody is left\n     on goes away. */\n  const toggle = (emoji: string, label: string) => {\n    const existing = reactions.find((reaction) => reaction.emoji === emoji)\n    const mine = !existing?.mine\n    const next = existing\n      ? reactions.map((reaction) =>\n          reaction.emoji === emoji ? { ...reaction, mine } : reaction\n        )\n      : [...reactions, { emoji, label, people: [], mine }]\n    const kept = next.filter((reaction) => countOf(reaction) > 0)\n    setReactions(kept)\n    setAnnouncement(\n      mine ? `You reacted with ${label}` : `Removed your ${label} reaction`\n    )\n    /* The pressed pill unmounts with its last reaction, and focus would\n       fall to the page; the add button sits beside the row. */\n    if (kept.length < next.length) triggerRef.current?.focus()\n  }\n\n  return (\n    <Item variant=\"outline\" className=\"max-w-md\">\n      {/* Top-aligned to the author line; an Item centres its media unless\n          the row has an ItemDescription. */}\n      <ItemMedia className=\"self-start\">\n        <Avatar>\n          <AvatarImage\n            src=\"https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80\"\n            alt=\"\"\n          />\n          <AvatarFallback>LM</AvatarFallback>\n        </Avatar>\n      </ItemMedia>\n      <ItemContent>\n        <ItemTitle>\n          Lucas Martin\n          <span\n            aria-hidden=\"true\"\n            className=\"bg-muted-foreground/40 size-1 shrink-0 rounded-full\"\n          />\n          <span className=\"text-muted-foreground font-normal\">10:42 AM</span>\n        </ItemTitle>\n        <p className=\"text-pretty\">\n          Dark mode is live for 10% of workspaces. If error rates hold, everyone\n          gets it on Thursday.\n        </p>\n        <TooltipProvider>\n          <div className=\"flex flex-wrap items-center gap-1.5 pt-2\">\n            {reactions.map((reaction) => {\n              const count = countOf(reaction)\n              /* Keyed by the emoji, not the index, so a description never\n               moves to a neighbour when a pill above it goes away. */\n              const descriptionId = `${id}-${reaction.emoji}`\n              return (\n                <Tooltip key={reaction.emoji}>\n                  {/* Yours are solid, everyone else's muted. */}\n                  <TooltipTrigger\n                    render={\n                      <Button\n                        variant={reaction.mine ? \"default\" : \"secondary\"}\n                        size=\"sm\"\n                        aria-pressed={reaction.mine}\n                        aria-label={`${reaction.label}, ${count} ${\n                          count === 1 ? \"reaction\" : \"reactions\"\n                        }`}\n                        aria-describedby={descriptionId}\n                        onClick={() => toggle(reaction.emoji, reaction.label)}\n                      />\n                    }\n                  >\n                    <span aria-hidden=\"true\" className=\"text-sm leading-none\">\n                      {reaction.emoji}\n                    </span>\n                    <span aria-hidden=\"true\" className=\"tabular-nums\">\n                      {count}\n                    </span>\n                    {/* Base UI keeps tooltip text from screen readers, so\n                      the names reach them through this instead. */}\n                    <span id={descriptionId} hidden>\n                      {namesOf(reaction)} reacted with {reaction.label}\n                    </span>\n                  </TooltipTrigger>\n                  <TooltipContent>{namesOf(reaction)}</TooltipContent>\n                </Tooltip>\n              )\n            })}\n            {/* Controlled to null so the trigger keeps its icon instead of\n              showing the last pick as a value. */}\n            <EmojiPicker\n              value={null}\n              aria-label=\"Add reaction\"\n              onEmojiSelect={({ value, emoji, skinTone }) =>\n                toggle(value, labelOf(emoji, skinTone))\n              }\n            >\n              <EmojiPickerTrigger\n                ref={triggerRef}\n                variant=\"secondary\"\n                size=\"icon-sm\"\n                title=\"Add reaction\"\n              >\n                <IconPlaceholder\n                  lucide=\"SmilePlusIcon\"\n                  tabler=\"IconMoodPlus\"\n                  hugeicons=\"SmilePlusIcon\"\n                  phosphor=\"SmileyIcon\"\n                  remixicon=\"RiEmotionLine\"\n                  aria-hidden=\"true\"\n                />\n              </EmojiPickerTrigger>\n              {/* Offset past the card's bottom edge, so the panel reads as\n                its own surface instead of clipping the corner. */}\n              <EmojiPickerContent side=\"bottom\" align=\"end\" sideOffset={20}>\n                <QuickReactions reactions={reactions} />\n                <EmojiPickerHeader>\n                  <EmojiPickerSearch />\n                </EmojiPickerHeader>\n                <EmojiPickerCategories />\n                <EmojiPickerList />\n                <EmojiPickerFooter>\n                  <EmojiPickerPreview />\n                  <EmojiPickerSkinTone />\n                </EmojiPickerFooter>\n              </EmojiPickerContent>\n            </EmojiPicker>\n          </div>\n        </TooltipProvider>\n        <p role=\"status\" className=\"sr-only\">\n          {announcement}\n        </p>\n      </ItemContent>\n    </Item>\n  )\n}","target":"components/examples/c-emoji-picker-3.tsx"}],"meta":{"order":3,"label":"Message Reactions"}}