{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reaction-bar",
  "title": "Reaction Bar",
  "description": "Reaction counts and selected states with accessible names and an add-reaction menu.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "toggle",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/reaction-bar.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { SmilePlus } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type FocusEvent,\n  type KeyboardEvent,\n  useContext,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const REACTION_BAR_VARIANTS = [\"pill\", \"outlined\", \"compact\"] as const;\nexport type ReactionBarVariant = (typeof REACTION_BAR_VARIANTS)[number];\nexport type ReactionBarProps = Omit<ComponentProps<\"div\">, \"defaultValue\"> & {\n  variant?: ReactionBarVariant;\n  /** Reactions selected by the current person. */\n  value?: readonly string[];\n  defaultValue?: readonly string[];\n  onValueChange?: (value: string[]) => void;\n  disabled?: boolean;\n};\ntype ReactionContext = {\n  variant: ReactionBarVariant;\n  selected: readonly string[];\n  disabled: boolean;\n  toggle: (value: string) => void;\n};\nconst Context = createContext<ReactionContext | null>(null);\nfunction useReactions(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within ReactionBar`);\n  return context;\n}\ntype PickerContext = { close: () => void };\nconst PickerContextValue = createContext<PickerContext | null>(null);\nfunction usePicker(part: string) {\n  const context = useContext(PickerContextValue);\n  if (!context) throw new Error(`${part} must be used within ReactionBarPicker`);\n  return context;\n}\nconst transitionChip =\n  \"transition-[background-color,color,box-shadow,scale] duration-[120ms,120ms,120ms,140ms] ease-[ease-out,ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\";\n\nconst reactionBarVariants = cva(\n  \"flex min-w-0 flex-wrap items-center text-[13px]/[18px] text-foreground\",\n  { variants: { variant: { pill: \"gap-1.5\", outlined: \"gap-1.5\", compact: \"gap-1\" } } },\n);\n\nconst reactionBarItemVariants = cva(\n  `group/reaction-item inline-flex min-w-0 cursor-pointer items-center rounded-full border-0 bg-secondary font-medium text-muted-foreground tabular-nums shadow-none ${transitionChip} enabled:hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] enabled:hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring enabled:active:scale-[0.94] disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:enabled:active:scale-100 data-[state=on]:bg-primary/14 data-pressed:bg-primary/14 data-[state=on]:text-primary-ink data-pressed:text-primary-ink data-[state=on]:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--primary)_42%,transparent)] data-pressed:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--primary)_42%,transparent)] data-[state=on]:enabled:hover:bg-primary/20 data-pressed:enabled:hover:bg-primary/20 data-[state=on]:enabled:hover:text-primary-ink data-pressed:enabled:hover:text-primary-ink data-[state=on]:enabled:hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--primary)_55%,transparent)] data-pressed:enabled:hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--primary)_55%,transparent)]`,\n  {\n    variants: {\n      variant: {\n        pill: \"h-7 gap-1.5 px-2.5 text-[12.5px]\",\n        outlined:\n          \"h-7 gap-1.5 bg-transparent px-2.5 text-[12.5px] shadow-[inset_0_0_0_1px_var(--border)] enabled:hover:bg-accent enabled:hover:shadow-[inset_0_0_0_1px_var(--border-strong)]\",\n        compact:\n          \"relative h-5.5 gap-1 px-[7px] text-[11.5px] after:absolute after:-inset-x-0.5 after:-inset-y-1 after:content-['']\",\n      },\n    },\n  },\n);\n\nexport function ReactionBar({\n  variant = \"pill\",\n  value,\n  defaultValue = [],\n  onValueChange,\n  disabled = false,\n  \"aria-label\": label = \"Reactions\",\n  className,\n  children,\n  ...props\n}: ReactionBarProps) {\n  const [internal, setInternal] = useState<readonly string[]>(defaultValue);\n  const selected = value ?? internal;\n  const toggle = (reaction: string) => {\n    if (disabled) return;\n    const updated = selected.includes(reaction)\n      ? selected.filter((item) => item !== reaction)\n      : [...selected, reaction];\n    if (value === undefined) setInternal(updated);\n    onValueChange?.(updated);\n  };\n  return (\n    <Context.Provider value={{ variant, selected, disabled, toggle }}>\n      {/* biome-ignore lint/a11y/useSemanticElements: a labelled group of toggle buttons, not a fieldset. */}\n      <div\n        data-slot=\"reaction-bar\"\n        role=\"group\"\n        aria-label={label}\n        className={cn(reactionBarVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\n\nexport type ReactionBarItemProps = Omit<ComponentProps<\"button\">, \"value\" | \"children\"> & {\n  value: string;\n  /** The emoji or icon shown in the button. */\n  emoji: React.ReactNode;\n  /** Spoken name, for example \"Thumbs up\". */\n  label: string;\n  /** Total count, including the current person's reaction. */\n  count: number;\n};\nexport function ReactionBarItem({\n  value,\n  emoji,\n  label,\n  count,\n  onClick,\n  className,\n  ...props\n}: ReactionBarItemProps) {\n  const context = useReactions(\"ReactionBarItem\");\n  const pressed = context.selected.includes(value);\n  const compact = context.variant === \"compact\";\n  // Pop only after the person toggles, not for reactions that load preselected.\n  const [toggled, setToggled] = useState(false);\n  return (\n    <Toggle\n      data-slot=\"reaction-bar-item\"\n      type=\"button\"\n      className={cn(reactionBarItemVariants({ variant: context.variant }), className)}\n      {...props}\n      pressed={pressed}\n      onPressedChange={() => {\n        setToggled(true);\n        context.toggle(value);\n      }}\n      aria-label={`${label}, ${count} ${count === 1 ? \"reaction\" : \"reactions\"}`}\n      disabled={context.disabled || props.disabled}\n      onClick={onClick}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"inline-block\",\n          toggled &&\n            pressed &&\n            \"animate-in duration-260 ease-[cubic-bezier(0.16,1,0.3,1)] zoom-in-70 motion-reduce:animate-none\",\n          compact ? \"text-[12px]/none\" : \"text-[14px]/none\",\n        )}\n      >\n        {emoji}\n      </span>\n      <span\n        key={count}\n        aria-hidden=\"true\"\n        className=\"inline-block animate-in duration-200 ease-out-quint fade-in-0 slide-in-from-bottom-1 motion-reduce:animate-none\"\n      >\n        {count}\n      </span>\n    </Toggle>\n  );\n}\n\nfunction menuItems(menu: HTMLElement | null) {\n  return Array.from(\n    menu?.querySelectorAll<HTMLElement>('[role^=\"menuitem\"]:not([data-disabled])') ?? [],\n  );\n}\n\nexport function ReactionBarPicker({\n  \"aria-label\": label = \"Add reaction\",\n  children,\n  className,\n  ...props\n}: ComponentProps<\"div\">) {\n  const context = useReactions(\"ReactionBarPicker\");\n  const [open, setOpen] = useState(false);\n  const focusTarget = useRef<\"first\" | \"last\">(\"first\");\n  const compact = context.variant === \"compact\";\n  const close = () => setOpen(false);\n  // ArrowUp on the trigger opens the menu on its last option.\n  const onMenuFocus = (event: FocusEvent<HTMLDivElement>) => {\n    if (event.target !== event.currentTarget) return;\n    const target = focusTarget.current;\n    focusTarget.current = \"first\";\n    if (target !== \"last\") return;\n    event.preventDefault();\n    menuItems(event.currentTarget).at(-1)?.focus();\n  };\n  // The picker is a horizontal row, so Left/Right rove like Up/Down. Up/Down past either end\n  // wrap here too, because Radix menus only loop with a Radix-only prop (Base UI menus loop).\n  const onMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];\n    if (step === undefined) return;\n    const items = menuItems(event.currentTarget);\n    const index = items.indexOf(document.activeElement as HTMLElement);\n    const next = index + step;\n    const vertical = event.key === \"ArrowDown\" || event.key === \"ArrowUp\";\n    if (vertical && next >= 0 && next < items.length) return;\n    event.preventDefault();\n    items[(next + items.length) % items.length]?.focus();\n  };\n  return (\n    <PickerContextValue.Provider value={{ close }}>\n      <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n        <div\n          data-slot=\"reaction-bar-picker\"\n          className={cn(\"relative inline-flex\", className)}\n          {...props}\n        >\n          <DropdownMenuTrigger\n            asChild\n            disabled={context.disabled}\n            onKeyDown={(event) => {\n              focusTarget.current = event.key === \"ArrowUp\" ? \"last\" : \"first\";\n              if (event.key === \"ArrowUp\") {\n                event.preventDefault();\n                setOpen(true);\n              }\n            }}\n          >\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label={label}\n              className={cn(\n                \"rounded-full border-0 bg-transparent p-0 text-muted-foreground transition-[background-color,color,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring enabled:hover:bg-accent enabled:hover:text-accent-foreground enabled:active:scale-[0.94] aria-expanded:bg-accent aria-expanded:text-accent-foreground motion-reduce:transition-none motion-reduce:enabled:active:scale-100 dark:hover:bg-accent\",\n                compact\n                  ? \"relative size-5.5 after:absolute after:-inset-x-0.5 after:-inset-y-1 after:content-[''] [&_svg:not([class*='size-'])]:size-[13px]\"\n                  : \"size-7 [&_svg:not([class*='size-'])]:size-[15px]\",\n              )}\n            >\n              <SmilePlus strokeWidth={1.75} aria-hidden=\"true\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            aria-label={label}\n            align=\"start\"\n            sideOffset={6}\n            onKeyDown={onMenuKeyDown}\n            onFocus={onMenuFocus}\n            className=\"z-20 flex min-w-0 gap-0.5 rounded-[14px] border-0 bg-popover p-1 text-popover-foreground shadow-[0_0_0_1px_var(--border-strong),0_12px_28px_-10px_oklch(0_0_0/0.32),0_2px_6px_-2px_oklch(0_0_0/0.12)] duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] data-[state=closed]:zoom-out-96 data-closed:zoom-out-96 data-[state=open]:zoom-in-96 data-open:zoom-in-96 data-[state=open]:[--tw-enter-translate-x:0]! data-open:[--tw-enter-translate-x:0]! data-[state=open]:[--tw-enter-translate-y:0]! data-open:[--tw-enter-translate-y:0]! motion-reduce:data-[state=closed]:animate-none motion-reduce:data-closed:animate-none motion-reduce:data-[state=open]:animate-none motion-reduce:data-open:animate-none\"\n          >\n            {children}\n          </DropdownMenuContent>\n        </div>\n      </DropdownMenu>\n    </PickerContextValue.Provider>\n  );\n}\n\nexport type ReactionBarPickerOptionProps = Omit<\n  ComponentProps<typeof DropdownMenuCheckboxItem>,\n  \"value\" | \"children\" | \"checked\" | \"onCheckedChange\"\n> & {\n  value: string;\n  emoji: React.ReactNode;\n  label: string;\n};\nexport function ReactionBarPickerOption({\n  value,\n  emoji,\n  label,\n  className,\n  ...props\n}: ReactionBarPickerOptionProps) {\n  const context = useReactions(\"ReactionBarPickerOption\");\n  usePicker(\"ReactionBarPickerOption\");\n  const checked = context.selected.includes(value);\n  return (\n    <DropdownMenuCheckboxItem\n      data-slot=\"reaction-bar-picker-option\"\n      className={cn(\n        \"group/reaction-option grid size-8 cursor-pointer place-items-center rounded-[10px] border-0 bg-transparent p-0 text-[16px]/none transition-[background-color,scale] duration-[120ms,140ms] ease-[ease-out,cubic-bezier(0.23,1,0.32,1)] focus:bg-accent focus:outline-none active:scale-[0.94] aria-checked:bg-primary/16 motion-reduce:transition-none motion-reduce:active:scale-100 [&>span:first-child]:hidden\",\n        className,\n      )}\n      {...props}\n      checked={checked}\n      onCheckedChange={() => context.toggle(value)}\n      aria-label={label}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"inline-block transition-[scale] duration-140 ease-out-quint group-hover/reaction-option:scale-[1.12] motion-reduce:transition-none motion-reduce:group-hover/reaction-option:scale-100\"\n      >\n        {emoji}\n      </span>\n    </DropdownMenuCheckboxItem>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/reaction-bar.tsx"
    }
  ],
  "categories": [
    "content",
    "community"
  ],
  "type": "registry:ui"
}