{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "social-pill",
  "type": "registry:component",
  "title": "Social Pill",
  "description": "A like, comment, repost and share pill where each icon pops and fills with a tiny burst and every count rolls over digit by digit.",
  "categories": [
    "component",
    "Together"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/social-pill.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, HeartIcon, MessageCircleIcon, Repeat2Icon, ShareIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst compact = new Intl.NumberFormat(\"en-US\", { notation: \"compact\", maximumFractionDigits: 1 });\nconst full = new Intl.NumberFormat(\"en-US\");\nconst format = (n: number) => (n < 10_000 ? full.format(n) : compact.format(n));\n\ntype Slot = { char: string; id: string };\n\nfunction toSlots(value: number, previous: Slot[], version: number): Slot[] {\n  return [...format(value)].reverse().map((char, i) => (previous[i]?.char === char ? previous[i] : { char, id: `${char}:${version}` }));\n}\n\nexport function RollingCount({ value, className }: { value: number; className?: string }) {\n  const [state, setState] = useState(() => ({ value, version: 0, direction: 1, slots: toSlots(value, [], 0) }));\n  if (value !== state.value) {\n    const version = state.version + 1;\n    setState({ value, version, direction: value > state.value ? 1 : -1, slots: toSlots(value, state.slots, version) });\n  }\n  const reduce = useReducedMotion();\n  const { direction } = state;\n  const slots = [...state.slots].reverse();\n\n  return (\n    <span className={cn(\"relative inline-flex tabular-nums\", className)}>\n      <span className=\"sr-only\">{value}</span>\n      <span aria-hidden className=\"inline-flex\">\n        {slots.map((slot, i) => {\n          const fromRight = slots.length - i;\n          return (\n            <span className=\"relative inline-flex h-[1lh] overflow-hidden\" key={fromRight}>\n              <AnimatePresence custom={direction} initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate={{ y: \"0%\", opacity: 1 }}\n                  className=\"inline-block\"\n                  custom={direction}\n                  exit={reduce ? { opacity: 0 } : { y: `${-100 * direction}%`, opacity: 0 }}\n                  initial={reduce ? { opacity: 0 } : { y: `${100 * direction}%`, opacity: 0 }}\n                  key={slot.id}\n                  transition={{ type: \"spring\", stiffness: 420, damping: 34, delay: reduce ? 0 : (fromRight - 1) * 0.03 }}\n                >\n                  {slot.char}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n\nfunction Burst({ trigger, className }: { trigger: number; className: string }) {\n  const reduce = useReducedMotion();\n  if (reduce || trigger === 0) return null;\n  return (\n    <span aria-hidden className=\"pointer-events-none absolute inset-0 flex items-center justify-center\" key={trigger}>\n      <motion.span\n        animate={{ scale: 1.9, opacity: 0, borderWidth: 0 }}\n        className={cn(\"absolute size-5 rounded-full border-2 border-current\", className)}\n        initial={{ scale: 0.3, opacity: 0.9, borderWidth: 3 }}\n        transition={{ duration: 0.5, ease: \"easeOut\" }}\n      />\n      {Array.from({ length: 7 }, (_, i) => {\n        const angle = (i / 7) * Math.PI * 2 - Math.PI / 2;\n        return (\n          <motion.span\n            animate={{ x: Math.cos(angle) * 16, y: Math.sin(angle) * 16, scale: 0, opacity: 0 }}\n            className={cn(\"absolute size-1.5 rounded-full bg-current\", className)}\n            initial={{ x: 0, y: 0, scale: 1, opacity: 1 }}\n            key={i}\n            transition={{ duration: 0.55, ease: [0.2, 0.8, 0.3, 1], delay: 0.05 }}\n          />\n        );\n      })}\n    </span>\n  );\n}\n\ninterface ActionProps {\n  label: string;\n  icon: ReactNode;\n  count?: number;\n  active?: boolean;\n  activeClass: string;\n  onClick: () => void;\n  pop: number;\n  burst?: boolean;\n  toggle?: boolean;\n}\n\nfunction Action({ label, icon, count, active, activeClass, onClick, pop, burst, toggle }: ActionProps) {\n  const reduce = useReducedMotion();\n  return (\n    <button\n      aria-label={label}\n      aria-pressed={toggle ? active : undefined}\n      className={cn(\n        \"group/action relative inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 font-medium text-muted-foreground text-sm outline-none transition-[color,background-color,scale] duration-150 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] [&_svg]:size-4.5\",\n        active ? activeClass : \"hover:text-foreground\",\n      )}\n      onClick={onClick}\n      type=\"button\"\n    >\n      <span className=\"relative flex size-5 items-center justify-center\">\n        {burst && <Burst className={activeClass} trigger={active ? pop : 0} />}\n        <motion.span\n          animate={reduce || pop === 0 ? { scale: 1, rotate: 0 } : { scale: [1, 0.8, 1.18, 1], rotate: active ? [0, -10, 6, 0] : 0 }}\n          className=\"flex\"\n          key={pop}\n          transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}\n        >\n          {icon}\n        </motion.span>\n      </span>\n      {count !== undefined && <RollingCount className=\"min-w-[1ch]\" value={count} />}\n    </button>\n  );\n}\n\nexport interface SocialPillProps {\n  likes?: number;\n  comments?: number;\n  reposts?: number;\n  liked?: boolean;\n  reposted?: boolean;\n  onLikeChange?: (liked: boolean) => void;\n  onRepostChange?: (reposted: boolean) => void;\n  onComment?: () => void;\n  onShare?: () => void;\n  className?: string;\n}\n\nexport function SocialPill({\n  likes = 0,\n  comments = 0,\n  reposts = 0,\n  liked: likedProp = false,\n  reposted: repostedProp = false,\n  onLikeChange,\n  onRepostChange,\n  onComment,\n  onShare,\n  className,\n}: SocialPillProps) {\n  const [liked, setLiked] = useState(likedProp);\n  const [reposted, setReposted] = useState(repostedProp);\n  const [pops, setPops] = useState({ like: 0, comment: 0, repost: 0, share: 0 });\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<number>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  const bump = (key: keyof typeof pops) => setPops((p) => ({ ...p, [key]: p[key] + 1 }));\n\n  return (\n    <div\n      className={cn(\"inline-flex items-center gap-0.5 rounded-full border bg-card p-1 shadow-xs/5\", className)}\n      role=\"group\"\n      aria-label=\"Reactions\"\n    >\n      <Action\n        activeClass=\"text-destructive-foreground\"\n        burst\n        count={likes - (likedProp ? 1 : 0) + (liked ? 1 : 0)}\n        active={liked}\n        icon={<HeartIcon className={cn(\"transition-[fill]\", liked && \"fill-current\")} />}\n        label={liked ? \"Unlike\" : \"Like\"}\n        onClick={() => {\n          bump(\"like\");\n          setLiked(!liked);\n          onLikeChange?.(!liked);\n        }}\n        pop={pops.like}\n        toggle\n      />\n      <Action\n        activeClass=\"text-info-foreground\"\n        count={comments}\n        icon={<MessageCircleIcon />}\n        label=\"Comment\"\n        onClick={() => {\n          bump(\"comment\");\n          onComment?.();\n        }}\n        pop={pops.comment}\n      />\n      <Action\n        activeClass=\"text-success-foreground\"\n        burst\n        count={reposts - (repostedProp ? 1 : 0) + (reposted ? 1 : 0)}\n        active={reposted}\n        icon={<Repeat2Icon className={cn(\"transition-transform duration-500\", reposted && \"rotate-180\")} />}\n        label={reposted ? \"Undo repost\" : \"Repost\"}\n        onClick={() => {\n          bump(\"repost\");\n          setReposted(!reposted);\n          onRepostChange?.(!reposted);\n        }}\n        pop={pops.repost}\n        toggle\n      />\n      <Action\n        activeClass=\"text-success-foreground\"\n        active={copied}\n        icon={\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              animate={{ scale: 1, opacity: 1 }}\n              className=\"flex\"\n              exit={{ scale: 0.6, opacity: 0 }}\n              initial={{ scale: 0.6, opacity: 0 }}\n              transition={{ duration: 0.16, ease: [0.23, 1, 0.32, 1] }}\n              key={copied ? \"check\" : \"share\"}\n            >\n              {copied ? <CheckIcon /> : <ShareIcon />}\n            </motion.span>\n          </AnimatePresence>\n        }\n        label={copied ? \"Link copied\" : \"Share\"}\n        onClick={() => {\n          bump(\"share\");\n          onShare?.();\n          setCopied(true);\n          clearTimeout(timer.current);\n          timer.current = window.setTimeout(() => setCopied(false), 1600);\n        }}\n        pop={pops.share}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}