{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kinetic-testimonials",
  "type": "registry:ui",
  "title": "Kinetic Testimonials",
  "description": "Smoothly scrolling, animated testimonials for modern developer websites to showcase user feedback visually.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": ["@scrollxui/avatar", "@scrollxui/card"],
  "dependencies": ["@radix-ui/react-avatar", "clsx", "tailwind-merge"],
  "css": {
    "@layer utilities": {
      ".animate-scroll-up": {
        "animation": "scroll-up-smooth linear infinite"
      },
      ".animate-scroll-down": {
        "animation": "scroll-down-smooth linear infinite"
      }
    },
    "@keyframes scroll-up-smooth": {
      "0%": {
        "transform": "translateY(0%)"
      },
      "100%": {
        "transform": "translateY(-50%)"
      }
    },
    "@keyframes scroll-down-smooth": {
      "0%": {
        "transform": "translateY(-50%)"
      },
      "100%": {
        "transform": "translateY(0%)"
      }
    }
  },
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/kinetic-testimonials.tsx",
      "content": "'use client';\nimport React, { useEffect, useState, useCallback, useMemo } from 'react';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\n\ninterface Testimonial {\n  name: string;\n  handle: string;\n  review: string;\n  avatar: string;\n}\n\ninterface TestimonialCardProps {\n  testimonial: Testimonial;\n  index: number;\n  cardClassName?: string;\n  avatarClassName?: string;\n}\n\ninterface KineticTestimonialProps {\n  testimonials?: Testimonial[];\n  className?: string;\n  cardClassName?: string;\n  avatarClassName?: string;\n  desktopColumns?: number;\n  tabletColumns?: number;\n  mobileColumns?: number;\n  speed?: number;\n  title?: string;\n  subtitle?: string;\n}\n\ninterface TestimonialWithId extends Testimonial {\n  uniqueId: string;\n}\n\nconst TestimonialCard: React.FC<TestimonialCardProps> = React.memo(\n  ({ testimonial, index, cardClassName = '', avatarClassName = '' }) => {\n    const [isHovered, setIsHovered] = useState<boolean>(false);\n\n    const gradients = [\n      'from-pink-500 via-purple-500 to-orange-400',\n      'from-blue-500 via-teal-500 to-green-400',\n      'from-purple-500 via-pink-500 to-red-400',\n      'from-indigo-500 via-blue-500 to-cyan-400',\n      'from-orange-500 via-red-500 to-pink-400',\n      'from-emerald-500 via-blue-500 to-purple-400',\n      'from-rose-500 via-fuchsia-500 to-indigo-400',\n      'from-amber-500 via-orange-500 to-red-400',\n    ];\n\n    const gradientClass = gradients[index % gradients.length];\n\n    return (\n      <div\n        className='w-full mb-4 shrink-0'\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n      >\n        <Card\n          className={`transition-all duration-300 pointer-events-none relative overflow-hidden ${\n            isHovered ? 'text-white shadow-2xl border-transparent' : ''\n          } ${cardClassName}`}\n        >\n          {isHovered && (\n            <div\n              className={`absolute inset-0 bg-linear-to-b ${gradientClass} z-0`}\n              style={{\n                maskImage:\n                  'linear-gradient(to bottom, transparent 40%, black 100%)',\n                WebkitMaskImage:\n                  'linear-gradient(to bottom, transparent 40%, black 100%)',\n              }}\n            />\n          )}\n\n          <CardContent className='p-4 md:p-6 relative z-10'>\n            <p className='text-sm md:text-base mb-4 leading-relaxed transition-colors duration-300 text-neutral-800 dark:text-neutral-200'>\n              \"{testimonial.review}\"\n            </p>\n\n            <div className='flex items-center space-x-3'>\n              <Avatar className={`w-8 md:w-10 h-8 md:h-10 ${avatarClassName}`}>\n                <AvatarImage src={testimonial.avatar} alt={testimonial.name} />\n                <AvatarFallback>\n                  {testimonial.name\n                    .split(' ')\n                    .map((n) => n[0])\n                    .join('')}\n                </AvatarFallback>\n              </Avatar>\n              <div className='min-w-0'>\n                <p\n                  className={`font-semibold text-xs md:text-sm ${\n                    isHovered ? 'text-white' : ''\n                  }`}\n                >\n                  {testimonial.name}\n                </p>\n                <p\n                  className={`text-xs ${\n                    isHovered ? 'text-white/80' : 'text-muted-foreground'\n                  }`}\n                >\n                  {testimonial.handle}\n                </p>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    );\n  },\n);\n\nTestimonialCard.displayName = 'TestimonialCard';\n\nconst KineticTestimonial: React.FC<KineticTestimonialProps> = ({\n  testimonials = [],\n  className = '',\n  cardClassName = '',\n  avatarClassName = '',\n  desktopColumns = 6,\n  tabletColumns = 3,\n  mobileColumns = 2,\n  speed = 1,\n  title = 'What developers are saying',\n  subtitle = 'Hear from the developer community about their experience with ScrollX-UI',\n}) => {\n  const [actualMobileColumns, setActualMobileColumns] = useState(mobileColumns);\n\n  useEffect(() => {\n    const updateColumns = () => {\n      const width = window.innerWidth;\n      if (width < 400) {\n        setActualMobileColumns(1);\n      } else {\n        setActualMobileColumns(mobileColumns);\n      }\n    };\n\n    updateColumns();\n    window.addEventListener('resize', updateColumns);\n    return () => window.removeEventListener('resize', updateColumns);\n  }, [mobileColumns]);\n\n  const createColumns = useCallback(\n    (numColumns: number) => {\n      if (!testimonials || testimonials.length === 0) {\n        return [];\n      }\n\n      const columns: TestimonialWithId[][] = [];\n      const testimonialsPerColumn = 10;\n\n      for (let i = 0; i < numColumns; i++) {\n        const columnTestimonials: TestimonialWithId[] = [];\n\n        for (let j = 0; j < testimonialsPerColumn; j++) {\n          const testimonialIndex = (i * 11 + j * 3) % testimonials.length;\n          columnTestimonials.push({\n            ...testimonials[testimonialIndex],\n            uniqueId: `${i}-${j}-${testimonialIndex}`,\n          });\n        }\n\n        columns.push([...columnTestimonials, ...columnTestimonials]);\n      }\n\n      return columns;\n    },\n    [testimonials],\n  );\n\n  const desktopColumnsData = useMemo(\n    () => createColumns(desktopColumns),\n    [createColumns, desktopColumns],\n  );\n  const fiveColumnsData = useMemo(() => createColumns(5), [createColumns]);\n  const fourColumnsData = useMemo(() => createColumns(4), [createColumns]);\n  const tabletColumnsData = useMemo(\n    () => createColumns(tabletColumns),\n    [createColumns, tabletColumns],\n  );\n  const mobileColumnsData = useMemo(\n    () => createColumns(actualMobileColumns),\n    [createColumns, actualMobileColumns],\n  );\n\n  const renderColumn = useCallback(\n    (\n      columnTestimonials: TestimonialWithId[],\n      colIndex: number,\n      prefix: string,\n      containerHeight: number,\n    ) => {\n      const moveUp = colIndex % 2 === 0;\n      const animationDuration = (40 + colIndex * 3) / speed;\n\n      return (\n        <div\n          key={`${prefix}-${colIndex}`}\n          className='flex-1 overflow-hidden relative testimonial-column'\n          style={{ height: `${containerHeight}px` }}\n        >\n          <div\n            className={`flex flex-col ${\n              moveUp ? 'animate-scroll-up' : 'animate-scroll-down'\n            }`}\n            style={{\n              animationDuration: `${animationDuration}s`,\n            }}\n          >\n            {columnTestimonials.map((testimonial, index) => (\n              <TestimonialCard\n                key={`${prefix}-${colIndex}-${testimonial.uniqueId}-${index}`}\n                testimonial={testimonial}\n                index={colIndex * 3 + index}\n                cardClassName={cardClassName}\n                avatarClassName={avatarClassName}\n              />\n            ))}\n          </div>\n        </div>\n      );\n    },\n    [speed, cardClassName, avatarClassName],\n  );\n\n  return (\n    <section\n      className={`py-12 md:py-12 bg-gray-50 dark:bg-black transition-colors duration-300 ${className}`}\n    >\n      <div className='relative w-full text-gray-900 dark:text-white py-8 md:py-20 flex flex-col items-center overflow-hidden px-4 md:px-6'>\n        <h2 className='text-2xl md:text-4xl font-bold text-center mb-4 bg-linear-to-r from-purple-600 to-pink-600 dark:from-purple-400 dark:to-pink-400 bg-clip-text text-transparent'>\n          {title}\n        </h2>\n        <p className='text-gray-600 dark:text-gray-400 mb-8 md:mb-12 text-center w-full max-w-2xl px-4 text-sm'>\n          {subtitle}\n        </p>\n\n        {testimonials && testimonials.length > 0 && (\n          <>\n            <div className='hidden xl:flex gap-4 w-full max-w-7xl overflow-hidden relative mx-4'>\n              <div className='absolute top-0 left-0 right-0 h-20 bg-linear-to-b from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n              <div className='absolute bottom-0 left-0 right-0 h-20 bg-linear-to-t from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n\n              {desktopColumnsData.map((columnTestimonials, colIndex) =>\n                renderColumn(columnTestimonials, colIndex, 'desktop', 800),\n              )}\n            </div>\n\n            <div className='hidden lg:flex xl:hidden gap-4 w-full max-w-6xl overflow-hidden relative mx-4'>\n              <div className='absolute top-0 left-0 right-0 h-20 bg-linear-to-b from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n              <div className='absolute bottom-0 left-0 right-0 h-20 bg-linear-to-t from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n\n              {createColumns(Math.max(desktopColumns - 1, 3)).map(\n                (columnTestimonials, colIndex) =>\n                  renderColumn(columnTestimonials, colIndex, 'five', 800),\n              )}\n            </div>\n\n            <div className='hidden md:flex lg:hidden gap-4 w-full max-w-5xl overflow-hidden relative mx-4'>\n              <div className='absolute top-0 left-0 right-0 h-20 bg-linear-to-b from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n              <div className='absolute bottom-0 left-0 right-0 h-20 bg-linear-to-t from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n\n              {createColumns(Math.max(desktopColumns - 2, 2)).map(\n                (columnTestimonials, colIndex) =>\n                  renderColumn(columnTestimonials, colIndex, 'four', 800),\n              )}\n            </div>\n\n            <div className='hidden sm:flex md:hidden gap-4 w-full max-w-4xl overflow-hidden relative mx-4'>\n              <div className='absolute top-0 left-0 right-0 h-20 bg-linear-to-b from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n              <div className='absolute bottom-0 left-0 right-0 h-20 bg-linear-to-t from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n\n              {tabletColumnsData.map((columnTestimonials, colIndex) =>\n                renderColumn(columnTestimonials, colIndex, 'tablet', 800),\n              )}\n            </div>\n\n            <div className='sm:hidden flex gap-3 w-full overflow-hidden relative px-4'>\n              <div className='absolute top-0 left-0 right-0 h-20 bg-linear-to-b from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n              <div className='absolute bottom-0 left-0 right-0 h-20 bg-linear-to-t from-gray-50 dark:from-black to-transparent z-10 pointer-events-none'></div>\n\n              {mobileColumnsData.map((columnTestimonials, colIndex) =>\n                renderColumn(columnTestimonials, colIndex, 'mobile', 600),\n              )}\n            </div>\n          </>\n        )}\n      </div>\n    </section>\n  );\n};\n\nexport default KineticTestimonial;\n"
    },
    {
      "type": "registry:ui",
      "path": "components/ui/avatar.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\nimport { cn } from '@/lib/utils';\n\ntype AvatarProps = React.ComponentPropsWithoutRef<\n  typeof AvatarPrimitive.Root\n> & {\n  variant?: 'close-friends' | 'normal' | 'none';\n};\n\nconst Avatar = React.forwardRef<\n  React.ComponentRef<typeof AvatarPrimitive.Root>,\n  AvatarProps\n>(({ className, variant = 'none', ...props }, ref) => {\n  const ringClass =\n    variant === 'close-friends'\n      ? 'bg-linear-to-tr from-green-400 to-green-600'\n      : variant === 'normal'\n        ? 'bg-[conic-gradient(at_top_right,#f09433,#e6683c,#dc2743,#cc2366,#bc1888,#f09433)]'\n        : '';\n\n  return variant === 'none' ? (\n    <AvatarPrimitive.Root\n      ref={ref}\n      className={cn(\n        'relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full',\n        className,\n      )}\n      {...props}\n    />\n  ) : (\n    <div\n      className={cn(\n        'h-14 w-14 rounded-full p-0.5',\n        ringClass,\n        'flex items-center justify-center',\n      )}\n    >\n      <div className='h-full w-full rounded-full bg-black flex items-center justify-center overflow-hidden'>\n        <AvatarPrimitive.Root\n          ref={ref}\n          className={cn(\n            'h-full w-full rounded-full overflow-hidden',\n            className,\n          )}\n          {...props}\n        />\n      </div>\n    </div>\n  );\n});\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ComponentRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn('h-full w-full object-cover not-prose', className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      'flex h-full w-full items-center justify-center rounded-full bg-muted text-sm font-medium',\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
    },
    {
      "type": "registry:ui",
      "path": "components/ui/card.tsx",
      "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nfunction Card({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card'\n      className={cn(\n        'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-xs',\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-header'\n      className={cn(\n        '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-title'\n      className={cn('leading-none font-semibold', className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-description'\n      className={cn('text-muted-foreground text-sm', className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-action'\n      className={cn(\n        'col-start-2 row-span-2 row-start-1 self-start justify-self-end',\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-content'\n      className={cn('px-6', className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='card-footer'\n      className={cn('flex items-center px-6 [.border-t]:pt-6', className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
