{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reaction-bar-clean","type":"registry:ui","title":"Reaction Bar (Clean)","description":"Like, comment, repost and share in one pill – icons pop and fill, counts roll in digit by digit","dependencies":[],"files":[{"path":"registry/reaction-bar-clean/reaction-bar-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\n\n/*\n * Reaction Bar (clean style) – like, comment, repost and share as a row of quiet chips, in a single self-contained file.\n * Tapping an action marks the chip as yours with a light blue tint, its icon pops, and the count rolls in digit by\n * digit while the chip glides to its new width. The styles are injected below, so no extra files are needed.\n */\n\nexport type ReactionKey = \"like\" | \"comment\" | \"repost\" | \"share\";\n\nexport type ReactionState = { count: number; active?: boolean };\n\nexport type ReactionBarProps = {\n  /** Starting counts and whether each action is already taken */\n  reactions?: Partial<Record<ReactionKey, ReactionState>>;\n  /** Called after an action was toggled, with its new state */\n  onReact?: (key: ReactionKey, state: Required<ReactionState>) => void;\n  /** Hide actions you don't need */\n  actions?: ReactionKey[];\n  className?: string;\n};\n\nconst labels: Record<ReactionKey, [string, string]> = {\n  like: [\"Like\", \"likes\"],\n  comment: [\"Comment\", \"comments\"],\n  repost: [\"Repost\", \"reposts\"],\n  share: [\"Share\", \"shares\"],\n};\n\n/** 1234 → \"1.2K\", like social apps – rounded so 999,950 reads \"1M\", not \"1000K\" */\nconst compact = new Intl.NumberFormat(\"en\", { notation: \"compact\", maximumFractionDigits: 1 });\nconst exact = new Intl.NumberFormat(\"en\");\nconst short = (n: number) => (n >= 10_000 && n < 1_000_000 ? compact.format(Math.round(n / 1000) * 1000) : compact.format(n));\n/** Counts are whole, never negative – NaN, Infinity and the like count as 0 */\nconst clean = (n: unknown) => (typeof n === \"number\" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0);\n\nexport function ReactionBar({ reactions = {}, onReact, actions: actionsProp = [\"like\", \"comment\", \"repost\", \"share\"], className }: ReactionBarProps) {\n  // Only known actions, each once\n  const actions = (Array.isArray(actionsProp) ? actionsProp : []).filter((k, i, all) => k in labels && all.indexOf(k) === i);\n  const [state, setState] = useState(() =>\n    Object.fromEntries(\n      (Object.keys(labels) as ReactionKey[]).map((k) => [k, { count: clean(reactions?.[k]?.count), active: reactions?.[k]?.active === true }]),\n    ) as Record<ReactionKey, Required<ReactionState>>,\n  );\n  const [popped, setPopped] = useState<Record<string, number>>({});\n\n  const toggle = (key: ReactionKey) => {\n    const cur = state[key];\n    const next = { active: !cur.active, count: Math.max(0, cur.count + (cur.active ? -1 : 1)) };\n    setState((s) => ({ ...s, [key]: next }));\n    // A new key restarts the pop animation on every tap\n    setPopped((p) => ({ ...p, [key]: (p[key] ?? 0) + 1 }));\n    onReact?.(key, next);\n  };\n\n  if (!actions.length) return null;\n\n  return (\n    <>\n      <style href=\"ui-reaction-bar-clean\" precedence=\"default\">\n        {css}\n      </style>\n      <div className={[\"ui-rbc\", className].filter(Boolean).join(\" \")} role=\"group\" aria-label=\"Reactions\">\n        {actions.map((key) => {\n          const { count, active } = state[key];\n          const [verb, noun] = labels[key];\n          return (\n            <button\n              key={key}\n              type=\"button\"\n              className=\"ui-rbc__btn\"\n              data-kind={key}\n              aria-pressed={active}\n              aria-label={`${verb}${count ? `, ${exact.format(count)} ${count === 1 ? noun.slice(0, -1) : noun}` : \"\"}`}\n              title={count >= 1000 ? `${exact.format(count)} ${noun}` : undefined}\n              onClick={() => toggle(key)}\n            >\n              <span className=\"ui-rbc__icon\" key={popped[key] ?? 0} data-pop={popped[key] ? true : undefined}>\n                {icons[key]}\n              </span>\n              <Count value={count} />\n            </button>\n          );\n        })}\n      </div>\n    </>\n  );\n}\n\n/** The count glides to its new width; each changed digit rolls in from below */\nfunction Count({ value }: { value: number }) {\n  const text = value > 0 ? short(value) : \"\";\n  const outer = useRef<HTMLSpanElement>(null);\n  const inner = useRef<HTMLSpanElement>(null);\n  const first = useRef(true);\n\n  useLayoutEffect(() => {\n    const o = outer.current;\n    const i = inner.current;\n    if (!o || !i) return;\n    o.style.width = `${i.offsetWidth}px`;\n    if (first.current) {\n      first.current = false;\n      requestAnimationFrame(() => o.setAttribute(\"data-smooth\", \"\"));\n    }\n  }, [text]);\n\n  return (\n    <span className=\"ui-rbc__count\" ref={outer} aria-hidden=\"true\">\n      <span className=\"ui-rbc__digits\" ref={inner}>\n        {text.split(\"\").map((d, i) => (\n          <span key={`${text.length - i}-${d}`} className=\"ui-rbc__digit\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\n/* ---------- Icons (stroke, rounded) ---------- */\n\nconst Svg = ({ children }: { children: ReactNode }) => (\n  <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.65\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n    {children}\n  </svg>\n);\n\nconst icons: Record<ReactionKey, ReactNode> = {\n  like: (\n    <Svg>\n      <path className=\"ui-rbc__fill\" d=\"M19.46 4c-2.68-1.65-5.02-.99-6.43.07-.57.43-.86.65-1.03.65s-.46-.22-1.03-.65C9.56 3.01 7.22 2.35 4.54 4 1.02 6.15.22 13.27 8.34 19.28 9.89 20.43 10.66 21 12 21s2.11-.57 3.66-1.72C23.78 13.27 22.98 6.15 19.46 4Z\" />\n    </Svg>\n  ),\n  comment: (\n    <Svg>\n      <path className=\"ui-rbc__fill\" d=\"M12 21.5c5.52 0 10-4.25 10-9.5S17.52 2.5 12 2.5 2 6.75 2 12c0 1.36.3 2.65.84 3.82.15.31.2.66.1 1l-.6 2.23a1.08 1.08 0 0 0 1.6 1.59l2.22-.6c.34-.09.69-.04 1 .1 1.17.55 2.46.86 4.84.86Z\" />\n    </Svg>\n  ),\n  repost: (\n    <Svg>\n      <path d=\"M16.5 3.5 19 6l-2.5 2.5\" />\n      <path d=\"M19 6H8a4 4 0 0 0-4 4v1\" />\n      <path d=\"M7.5 20.5 5 18l2.5-2.5\" />\n      <path d=\"M5 18h11a4 4 0 0 0 4-4v-1\" />\n    </Svg>\n  ),\n  share: (\n    <Svg>\n      <path d=\"M21 3 10.5 13.5\" />\n      <path d=\"M21 3 14.6 20.6a.5.5 0 0 1-.93.05L10.5 13.5 3.35 10.33a.5.5 0 0 1 .05-.93Z\" />\n    </Svg>\n  ),\n};\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n.ui-rbc {\n  --ui-rbc-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-rbc-text: var(--aui-muted, rgba(0, 0, 0, 0.61));\n  --ui-rbc-icon: var(--aui-muted, rgba(0, 0, 0, 0.447));\n  --ui-rbc-line: var(--aui-border, rgba(0, 0, 0, 0.06));\n  --ui-rbc-wash: var(--aui-subtle, rgba(0, 0, 0, 0.03));\n  --ui-rbc-accent: rgba(0, 96, 226, 0.765);\n  --ui-rbc-mine: rgba(0, 96, 226, 0.05);\n  --ui-rbc-mine-line: rgba(0, 96, 226, 0.35);\n  --ui-rbc-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-rbc-ease: cubic-bezier(0.85, 0, 0.15, 1);\n  --ui-rbc-h: 28px;\n  display: inline-flex;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: 4px;\n  max-width: 100%;\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-rbc-text);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-rbc *, .ui-rbc *::before, .ui-rbc *::after { box-sizing: border-box; }\n:where(.ui-rbc) button { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; cursor: pointer; }\n:where(.ui-rbc) svg { display: block; flex: none; }\n\n/* A chip: the tab-chip look – inset hairline, radius 6. Without a count it is a square-ish icon chip */\n.ui-rbc__btn {\n  position: relative;\n  display: inline-flex;\n  flex: none;\n  align-items: center;\n  height: var(--ui-rbc-h);\n  padding: 0 8px;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  box-shadow: inset 0 0 0 0.5px var(--ui-rbc-line);\n  color: var(--ui-rbc-text);\n  transition: background-color 90ms ease, box-shadow 160ms ease, color 160ms ease;\n  -webkit-tap-highlight-color: transparent;\n}\n.ui-rbc__btn:focus-visible { outline: 2px solid var(--ui-rbc-focus); outline-offset: 2px; }\n@media (hover: hover) {\n  .ui-rbc__btn:hover { background-color: var(--ui-rbc-wash); color: var(--ui-rbc-ink); }\n  .ui-rbc__btn:hover .ui-rbc__icon { color: var(--ui-rbc-ink); }\n}\n/* Your own reaction: a minimal blue tint with a light blue hairline */\n.ui-rbc__btn[aria-pressed=\"true\"],\n.ui-rbc__btn[aria-pressed=\"true\"]:hover {\n  background-color: var(--ui-rbc-mine);\n  box-shadow: inset 0 0 0 0.5px var(--ui-rbc-mine-line);\n  color: var(--ui-rbc-accent);\n}\n\n.ui-rbc__icon { display: grid; place-items: center; flex: none; width: 16px; height: 16px; color: var(--ui-rbc-icon); transition: color 160ms ease; }\n.ui-rbc__fill { fill: transparent; transition: fill 160ms ease; }\n.ui-rbc__btn[aria-pressed=\"true\"] .ui-rbc__icon { color: var(--ui-rbc-accent); }\n.ui-rbc__btn[aria-pressed=\"true\"] .ui-rbc__fill { fill: currentColor; }\n/* Press pop: shrinks, springs past full size, settles */\n.ui-rbc__icon[data-pop] { animation: ui-rbc-pop 320ms cubic-bezier(0.65, 0, 0.35, 1); }\n\n/* Count: the width glides, digits roll */\n.ui-rbc__count {\n  display: inline-block;\n  overflow: hidden;\n  width: 0;\n  height: 20px;\n}\n.ui-rbc__count[data-smooth] { transition: width 400ms var(--ui-rbc-ease); }\n.ui-rbc__digits {\n  display: inline-flex;\n  padding: 0 2px 0 6px;\n  line-height: 20px;\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n.ui-rbc__digits:empty { padding: 0; }\n.ui-rbc__digit { display: inline-block; animation: ui-rbc-roll 400ms var(--ui-rbc-ease) both; }\n\n@keyframes ui-rbc-pop {\n  0% { transform: scale(1); }\n  35% { transform: scale(0.75); }\n  70% { transform: scale(1.12); }\n  100% { transform: scale(1); }\n}\n@keyframes ui-rbc-roll {\n  from { opacity: 0; transform: translateY(70%); }\n}\n\n/* Touch screens: taller chips */\n@media (hover: none) and (pointer: coarse) {\n  .ui-rbc { --ui-rbc-h: 36px; gap: 6px; }\n  .ui-rbc__btn { padding: 0 10px; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ui-rbc__icon[data-pop], .ui-rbc__digit { animation: none; }\n  .ui-rbc__count[data-smooth] { transition: none; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-rbc {\n  --ui-rbc-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-rbc-text: var(--aui-muted, rgba(255, 255, 255, 0.62));\n  --ui-rbc-icon: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-rbc-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-rbc-wash: var(--aui-subtle, rgba(255, 255, 255, 0.04));\n  --ui-rbc-accent: var(--aui-blue, #4fb0fe);\n  --ui-rbc-mine: rgba(77, 141, 255, 0.14);\n  --ui-rbc-mine-line: rgba(77, 141, 255, 0.4);\n  --ui-rbc-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n}\n`;\n"}]}