{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "native-likes-counter-baseui",
  "type": "registry:component",
  "title": "Native Likes Counter",
  "description": "An interactive likes counter with avatar stack, popup details, and smooth animations. (Base UI)",
  "registryDependencies": [
    "button"
  ],
  "dependencies": [
    "framer-motion",
    "react"
  ],
  "files": [
    {
      "path": "@uitripled/react-baseui/src/components/native/native-likes-counter-baseui.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar } from \"@base-ui/react/avatar\"\nimport { AnimatePresence, motion, MotionConfig } from \"framer-motion\"\nimport { Heart, Loader2 } from \"lucide-react\"\nimport { useState, useCallback, useEffect, useRef } from \"react\"\n\nexport interface LikeUser {\n  id: string\n  name: string\n  avatar?: string\n}\n\nexport interface NativeLikesCounterProps {\n  count: number\n  users?: LikeUser[]\n  variant?: \"default\" | \"subtle\" | \"outline\" | \"ghost\"\n  size?: \"sm\" | \"default\" | \"lg\"\n  liked?: boolean\n  onLike?: () => void\n  onLoadMore?: () => Promise<LikeUser[]> | LikeUser[]\n  hasMore?: boolean\n  maxAvatars?: number\n  maxVisibleInPopup?: number\n  className?: string\n}\n\nconst sizeVariants = {\n  sm: {\n    container: \"h-7 px-2.5 gap-1.5 text-xs\",\n    icon: \"w-3.5 h-3.5\",\n    avatar: \"w-4 h-4\",\n    avatarStack: \"-space-x-1\",\n    popup: \"p-3\",\n    popupAvatar: \"w-6 h-6\",\n  },\n  default: {\n    container: \"h-8 px-3 gap-2 text-sm\",\n    icon: \"w-4 h-4\",\n    avatar: \"w-5 h-5\",\n    avatarStack: \"-space-x-1.5\",\n    popup: \"p-3\",\n    popupAvatar: \"w-7 h-7\",\n  },\n  lg: {\n    container: \"h-9 px-3.5 gap-2 text-sm\",\n    icon: \"w-[18px] h-[18px]\",\n    avatar: \"w-6 h-6\",\n    avatarStack: \"-space-x-2\",\n    popup: \"p-3\",\n    popupAvatar: \"w-8 h-8\",\n  },\n}\n\nconst countVariants = {\n  enter: (direction: number) => ({ y: direction * -8, opacity: 0 }),\n  center: { y: 0, opacity: 1 },\n  exit: (direction: number) => ({ y: direction * 8, opacity: 0 }),\n}\n\nexport function NativeLikesCounterBaseUI({\n  count,\n  users = [],\n  variant = \"default\",\n  size = \"default\",\n  liked = false,\n  onLike,\n  onLoadMore,\n  hasMore = false,\n  maxAvatars = 5,\n  maxVisibleInPopup = 5,\n  className,\n}: NativeLikesCounterProps) {\n  const [isOpen, setIsOpen] = useState(false)\n  const [isLiked, setIsLiked] = useState(liked)\n  const [localCount, setLocalCount] = useState(count)\n  const [loadedUsers, setLoadedUsers] = useState<LikeUser[]>(users)\n  const [isLoadingMore, setIsLoadingMore] = useState(false)\n  const [canLoadMore, setCanLoadMore] = useState(hasMore)\n\n  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n  const hasInteracted = useRef(false)\n  const countDirection = useRef(1)\n\n  useEffect(\n    () => () => {\n      if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current)\n    },\n    [],\n  )\n\n  const sizeConfig = sizeVariants[size]\n  const displayUsers = loadedUsers.slice(0, maxAvatars)\n\n  const openPopup = () => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current)\n      hoverTimeoutRef.current = null\n    }\n    setIsOpen(true)\n  }\n\n  const handleMouseLeave = () => {\n    hoverTimeoutRef.current = setTimeout(() => {\n      setIsOpen(false)\n    }, 150) // Small delay to allow moving to popup\n  }\n\n  const handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {\n    if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n      setIsOpen(false)\n    }\n  }\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\") setIsOpen(false)\n  }\n\n  const handleLike = () => {\n    hasInteracted.current = true\n    countDirection.current = isLiked ? -1 : 1\n    setIsLiked(!isLiked)\n    setLocalCount((prev) => (isLiked ? prev - 1 : prev + 1))\n    onLike?.()\n  }\n\n  const handleLoadMore = useCallback(async () => {\n    if (!onLoadMore || isLoadingMore) return\n\n    setIsLoadingMore(true)\n    try {\n      const newUsers = await onLoadMore()\n      if (newUsers.length === 0) {\n        setCanLoadMore(false)\n      } else {\n        setLoadedUsers((prev) => [...prev, ...newUsers])\n      }\n    } catch (error) {\n      console.error(\"Failed to load more users:\", error)\n    } finally {\n      setIsLoadingMore(false)\n    }\n  }, [onLoadMore, isLoadingMore])\n\n  const getVariantStyles = () => {\n    const base = \"transition-colors duration-150\"\n    switch (variant) {\n      case \"subtle\":\n        return cn(base, \"bg-accent/50 hover:bg-accent\", isLiked && \"bg-accent\")\n      case \"outline\":\n        return cn(\n          base,\n          \"bg-transparent border border-border hover:border-accent-foreground/20 hover:bg-accent/10\",\n          isLiked && \"border-accent-foreground/30 bg-accent/20\",\n        )\n      case \"ghost\":\n        return cn(base, \"bg-transparent hover:bg-accent/50\", isLiked && \"bg-accent/30\")\n      default:\n        return cn(\n          base,\n          \"bg-accent border border-border hover:bg-accent/80 hover:border-accent-foreground/20\",\n          isLiked && \"border-accent-foreground/20\",\n        )\n    }\n  }\n\n  const visibleUsersInPopup = loadedUsers.slice(0, maxVisibleInPopup)\n  const totalRemaining = localCount - loadedUsers.length\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        className=\"relative inline-block\"\n        onMouseEnter={openPopup}\n        onMouseLeave={handleMouseLeave}\n        onFocus={openPopup}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n      >\n        <motion.button\n          type=\"button\"\n          onClick={handleLike}\n          aria-pressed={isLiked}\n          className={cn(\n            \"relative flex cursor-pointer items-center rounded-md font-medium\",\n            \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            sizeConfig.container,\n            getVariantStyles(),\n            className,\n          )}\n          whileTap={{ scale: 0.98 }}\n          transition={{ duration: 0.1 }}\n        >\n          {/* Heart icon */}\n          <span aria-hidden=\"true\" className=\"relative flex items-center justify-center\">\n            <motion.span\n              className=\"flex\"\n              animate={isLiked && hasInteracted.current ? { scale: [1, 1.15, 1] } : { scale: 1 }}\n              transition={{ duration: 0.2 }}\n            >\n              <Heart\n                className={cn(\n                  sizeConfig.icon,\n                  \"transition-colors duration-150\",\n                  isLiked ? \"fill-red-500 text-red-500\" : \"text-muted-foreground\",\n                )}\n              />\n            </motion.span>\n          </span>\n\n          <AnimatePresence mode=\"popLayout\" initial={false} custom={countDirection.current}>\n            <motion.span\n              key={localCount}\n              custom={countDirection.current}\n              variants={countVariants}\n              initial=\"enter\"\n              animate=\"center\"\n              exit=\"exit\"\n              transition={{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }}\n              className={cn(\"font-medium tabular-nums\", isLiked ? \"text-foreground\" : \"text-muted-foreground\")}\n            >\n              {localCount.toLocaleString()}\n            </motion.span>\n          </AnimatePresence>\n          <span className=\"sr-only\">likes</span>\n\n          {displayUsers.length > 0 && variant !== \"ghost\" && (\n            <div aria-hidden=\"true\" className={cn(\"flex items-center\", sizeConfig.avatarStack)}>\n              {displayUsers.map((user, index) => (\n                <motion.div\n                  key={user.id}\n                  initial={{ scale: 0.9, opacity: 0 }}\n                  animate={{ scale: 1, opacity: 1 }}\n                  transition={{ delay: index * 0.03, duration: 0.15 }}\n                >\n                  <Avatar.Root\n                    className={cn(\n                      sizeConfig.avatar,\n                      \"relative flex shrink-0 overflow-hidden rounded-full border border-background ring-1 ring-border\",\n                    )}\n                  >\n                    <Avatar.Image src={user.avatar || \"/placeholder.svg\"} alt=\"\" className=\"h-full w-full object-cover\" />\n                    <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-accent text-[9px] text-muted-foreground\">\n                      {user.name.charAt(0).toUpperCase()}\n                    </Avatar.Fallback>\n                  </Avatar.Root>\n                </motion.div>\n              ))}\n            </div>\n          )}\n        </motion.button>\n\n        <AnimatePresence>\n          {isOpen && loadedUsers.length > 0 && (\n            <motion.div\n              initial={{ opacity: 0, y: 4 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: 4 }}\n              transition={{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }}\n              style={{ x: \"-50%\" }}\n              className={cn(\n                \"absolute left-1/2 bottom-full mb-1 z-[100]\",\n                \"bg-popover border border-border rounded-lg shadow-2xl\",\n                \"w-[240px]\",\n                sizeConfig.popup,\n              )}\n            >\n              {/* Header */}\n              <div className=\"flex items-center justify-between mb-2 px-1\">\n                <span className=\"text-xs font-medium text-muted-foreground\">Liked by</span>\n                <span className=\"text-xs tabular-nums text-muted-foreground\">{localCount.toLocaleString()}</span>\n              </div>\n\n              <div className=\"max-h-[140px] overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent\">\n                <div className=\"space-y-1 px-1\">\n                  {visibleUsersInPopup.map((user, index) => (\n                    <motion.div\n                      key={user.id}\n                      initial={{ opacity: 0, x: -8 }}\n                      animate={{ opacity: 1, x: 0 }}\n                      transition={{\n                        delay: index * 0.02,\n                        duration: 0.15,\n                        ease: [0.23, 1, 0.32, 1],\n                      }}\n                      className=\"flex items-center gap-2 py-1 group\"\n                    >\n                      <Avatar.Root\n                        className={cn(\n                          sizeConfig.popupAvatar,\n                          \"relative flex shrink-0 overflow-hidden rounded-full border border-border\",\n                        )}\n                      >\n                        <Avatar.Image src={user.avatar || \"/placeholder.svg\"} alt=\"\" className=\"h-full w-full object-cover\" />\n                        <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-accent text-[10px] text-muted-foreground\">\n                          {user.name.charAt(0).toUpperCase()}\n                        </Avatar.Fallback>\n                      </Avatar.Root>\n                      <span className=\"text-xs text-foreground/80 group-hover:text-foreground transition-colors truncate\">\n                        {user.name}\n                      </span>\n                    </motion.div>\n                  ))}\n                </div>\n              </div>\n\n              {(canLoadMore || totalRemaining > 0) && (\n                <motion.div\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={{ delay: visibleUsersInPopup.length * 0.02 }}\n                  className=\"mt-2 pt-2 border-t border-border/50\"\n                >\n                  {onLoadMore && canLoadMore ? (\n                    <button\n                      type=\"button\"\n                      onClick={(e) => {\n                        e.stopPropagation()\n                        handleLoadMore()\n                      }}\n                      disabled={isLoadingMore}\n                      aria-busy={isLoadingMore}\n                      className=\"w-full flex items-center justify-center gap-1.5 py-1.5 rounded-md text-xs text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                    >\n                      {isLoadingMore ? (\n                        <>\n                          <Loader2 aria-hidden=\"true\" className=\"w-3 h-3 animate-spin\" />\n                          <span>Loading...</span>\n                        </>\n                      ) : (\n                        <span>Load more {totalRemaining > 0 && `(${totalRemaining.toLocaleString()} more)`}</span>\n                      )}\n                    </button>\n                  ) : totalRemaining > 0 ? (\n                    <div className=\"flex items-center justify-center py-1\">\n                      <span className=\"text-xs tabular-nums text-muted-foreground\">\n                        +{totalRemaining.toLocaleString()} others\n                      </span>\n                    </div>\n                  ) : null}\n                </motion.div>\n              )}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/uitripled/native-likes-counter-baseui.tsx"
    }
  ]
}