{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "animated-review-cards", "dependencies": [ "framer-motion", "class-variance-authority", "lucide-react" ], "registryDependencies": [ "https://reusables.vercel.app/r/utils.json", "avatar", "https://reusables.vercel.app/r/border-beam", "https://reusables.vercel.app/r/use-screen-size" ], "files": [ { "path": "registry/reusables/animated-review-cards.tsx", "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport { AnimatePresence, motion } from \"framer-motion\"\nimport { Star } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { BorderBeam } from \"@/components/border-beam\"\n\nimport useScreenSize from \"@/hooks/use-screen-size\"\n\ninterface Review {\n id: number | string\n name: string\n avatar: string\n text: string\n rating: number\n}\n\ntype ThemeColor = \"default\" | \"primary\" | \"elegant\" | \"vibrant\" | \"minimal\"\n\ninterface AnimatedReviewCardsProps {\n reviews?: Review[]\n interactionType?: \"drag\" | \"click\"\n animationDuration?: number\n scaleStep?: number\n verticalSpacing?: number\n horizontalSpacing?: number\n autoRotate?: boolean\n rotateInterval?: number\n theme?: ThemeColor\n showBorderBeam?: boolean\n classNames?: {\n container?: string\n card?: string\n cardContent?: string\n header?: string\n avatar?: string\n name?: string\n text?: string\n rating?: string\n star?: string\n activeStarColor?: string\n inactiveStarColor?: string\n }\n}\n\nconst cardVariants = cva(\n \"absolute h-[300px] w-[300px] overflow-hidden rounded-lg bg-background sm:w-[350px] md:h-[250px] md:w-[550px]\",\n {\n variants: {\n theme: {\n default: \"border border-border bg-background\",\n primary: \"bg-primary-50 border border-primary/20\",\n elegant:\n \"border border-zinc-200 bg-zinc-50 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100\",\n vibrant:\n \"border border-fuchsia-400 bg-gradient-to-br from-fuchsia-500 to-pink-500 text-white dark:border-fuchsia-700 dark:from-fuchsia-600 dark:to-pink-600\",\n minimal:\n \"border border-gray-100 bg-gray-50 text-gray-900 dark:border-gray-900 dark:bg-gray-950 dark:text-gray-100\",\n },\n cursor: {\n drag: \"cursor-grab active:cursor-grabbing\",\n click: \"cursor-pointer\",\n },\n },\n }\n)\n\nconst nameVariants = cva(\"text-lg font-semibold\", {\n variants: {\n theme: {\n default: \"text-foreground\",\n primary: \"text-primary\",\n secondary: \"text-secondary\",\n elegant: \"text-zinc-900 dark:text-zinc-100\",\n vibrant: \"text-white\",\n minimal: \"text-gray-900 dark:text-gray-100\",\n },\n },\n})\n\nconst textVariants = cva(\"text-start text-sm select-none\", {\n variants: {\n theme: {\n default: \"text-foreground\",\n primary: \"text-primary/80\",\n elegant: \"text-zinc-600 dark:text-zinc-300\",\n vibrant: \"text-white/90\",\n minimal: \"text-gray-600 dark:text-gray-400\",\n },\n },\n})\n\nconst starColorVariants = {\n default: {\n active: \"text-yellow-400 fill-current\",\n inactive: \"text-muted stroke-muted-foreground/20\",\n },\n primary: {\n active: \"text-primary\",\n inactive: \"text-primary/20\",\n },\n elegant: {\n active: \"text-zinc-700 dark:text-zinc-300 fill-current\",\n inactive: \"text-zinc-300 dark:text-zinc-600\",\n },\n vibrant: {\n active: \"text-white fill-current\",\n inactive: \"text-white/40\",\n },\n minimal: {\n active: \"text-gray-900 dark:text-gray-100 fill-current\",\n inactive: \"text-gray-200 dark:text-gray-700\",\n },\n}\n\nexport const AnimatedReviewCards = ({\n reviews: initialReviewsProp = [],\n interactionType = \"drag\",\n animationDuration = 0.3,\n scaleStep = 0.05,\n verticalSpacing = 10,\n horizontalSpacing = 20,\n autoRotate = true,\n rotateInterval = 6000,\n theme = \"default\",\n showBorderBeam = true,\n classNames,\n}: AnimatedReviewCardsProps) => {\n const starColors = starColorVariants[theme]\n const [reviews, setReviews] = useState(initialReviewsProp)\n const [isInteracting, setIsInteracting] = useState(false)\n const { isMobile } = useScreenSize()\n\n const handleInteraction = (index: number) => {\n setReviews((prevReviews) => {\n const newReviews = [...prevReviews]\n const [removed] = newReviews.splice(index, 1)\n newReviews.push(removed)\n return newReviews\n })\n }\n\n useEffect(() => {\n let intervalId: NodeJS.Timeout | null = null\n\n if (autoRotate && !isInteracting) {\n intervalId = setInterval(() => {\n handleInteraction(0)\n }, rotateInterval)\n }\n\n return () => {\n if (intervalId) {\n clearInterval(intervalId)\n }\n }\n }, [autoRotate, rotateInterval, isInteracting])\n\n return (\n \n \n {reviews.map((review, index) => (\n setIsInteracting(true)}\n onDragEnd={() => {\n setIsInteracting(false)\n interactionType === \"drag\" && handleInteraction(index)\n }}\n onClick={() => {\n if (interactionType === \"click\") {\n setIsInteracting(true)\n handleInteraction(index)\n setTimeout(() => setIsInteracting(false), 300)\n }\n }}\n title={interactionType === \"drag\" ? \"Drag me\" : \"Click me\"}\n className={cardVariants({\n theme,\n cursor: interactionType,\n className: classNames?.card,\n })}\n >\n \n
\n \n \n {review?.name.charAt(0)}\n \n \n {review?.name}\n \n
\n \n {review?.text}\n

\n \n {[...Array(5)].map((_, i) => (\n \n ))}\n \n {index === 0 && showBorderBeam && (\n \n )}\n \n \n ))}\n
\n \n )\n}\n", "type": "registry:component", "target": "components/animated-review-cards.tsx" } ], "type": "registry:component" }