{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "native-likes-counter-shadcnui", "type": "registry:component", "title": "Native Likes Counter", "description": "An interactive likes counter with avatar stack, popup details, and smooth animations.", "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/native/native-likes-counter-shadcnui.tsx", "content": "\"use client\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { cn } from \"@/lib/utils\"\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[]\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 NativeLikesCounter({\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(users)\n const [isLoadingMore, setIsLoadingMore] = useState(false)\n const [canLoadMore, setCanLoadMore] = useState(hasMore)\n\n const hoverTimeoutRef = useRef(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) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setIsOpen(false)\n }\n }\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\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 \n \n \n {/* Heart icon */}\n \n \n \n \n \n\n \n \n {localCount.toLocaleString()}\n \n \n likes\n\n {displayUsers.length > 0 && variant !== \"ghost\" && (\n
\n {displayUsers.map((user, index) => (\n \n \n \n \n {user.name.charAt(0).toUpperCase()}\n \n \n \n ))}\n
\n )}\n \n\n \n {isOpen && loadedUsers.length > 0 && (\n \n {/* Header */}\n
\n Liked by\n {localCount.toLocaleString()}\n
\n\n
\n
\n {visibleUsersInPopup.map((user, index) => (\n \n \n \n \n {user.name.charAt(0).toUpperCase()}\n \n \n \n {user.name}\n \n \n ))}\n
\n
\n\n {(canLoadMore || totalRemaining > 0) && (\n \n {onLoadMore && canLoadMore ? (\n {\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 \n Loading...\n \n ) : (\n Load more {totalRemaining > 0 && `(${totalRemaining.toLocaleString()} more)`}\n )}\n \n ) : totalRemaining > 0 ? (\n
\n \n +{totalRemaining.toLocaleString()} others\n \n
\n ) : null}\n \n )}\n \n )}\n
\n \n
\n )\n}\n", "type": "registry:component", "target": "components/uitripled/native-likes-counter-shadcnui.tsx" } ] }