{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "testimonial-carousel",
  "type": "registry:ui",
  "title": "Testimonial Carousel",
  "description": "Minimal, responsive testimonial slider with avatars, names, and quotes for clean, modern design.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [
    "@scrollxui/avatar",
    "@scrollxui/button",
    "@scrollxui/card"
  ],
  "dependencies": [
    "embla-carousel-react",
    "@radix-ui/react-avatar",
    "lucide-react",
    "@radix-ui/react-slot",
    "class-variance-authority",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/testimonial-carousel.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from 'embla-carousel-react';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\n\ntype TestimonialCardApi = UseEmblaCarouselType[1];\ntype UseTestimonialCardParameters = Parameters<typeof useEmblaCarousel>;\ntype TestimonialCardOptions = UseTestimonialCardParameters[0];\ntype TestimonialCardPlugin = UseTestimonialCardParameters[1];\n\ntype TestimonialCardProps = {\n  opts?: TestimonialCardOptions;\n  plugins?: TestimonialCardPlugin;\n\n  setApi?: (api: TestimonialCardApi) => void;\n};\n\ntype TestimonialCardContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & TestimonialCardProps;\n\nconst TestimonialCardContext =\n  React.createContext<TestimonialCardContextProps | null>(null);\n\nfunction useTestimonialCard() {\n  const context = React.useContext(TestimonialCardContext);\n  if (!context)\n    throw new Error(\n      'useTestimonialCard must be used within a <TestimonialCard />',\n    );\n  return context;\n}\n\nfunction TestimonialCard({\n  opts,\n  setApi,\n  plugins,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<'div'> & TestimonialCardProps) {\n  const [carouselRef, api] = useEmblaCarousel({ ...opts, loop: true }, plugins);\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n  const onSelect = React.useCallback((api: TestimonialCardApi) => {\n    if (!api) return;\n    setCanScrollPrev(api.canScrollPrev());\n    setCanScrollNext(api.canScrollNext());\n  }, []);\n\n  const scrollPrev = React.useCallback(() => {\n    if (api) api.scrollPrev();\n  }, [api]);\n  const scrollNext = React.useCallback(() => {\n    if (api) api.scrollNext();\n  }, [api]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === 'ArrowLeft') {\n        event.preventDefault();\n        scrollPrev();\n      } else if (event.key === 'ArrowRight') {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [scrollPrev, scrollNext],\n  );\n\n  React.useEffect(() => {\n    if (api && setApi) setApi(api);\n  }, [api, setApi]);\n\n  React.useEffect(() => {\n    if (!api) return;\n    onSelect(api);\n    api.on('reInit', onSelect);\n    api.on('select', onSelect);\n    return () => {\n      if (api) {\n        api.off('reInit', onSelect);\n        api.off('select', onSelect);\n      }\n    };\n  }, [api, onSelect]);\n\n  return (\n    <TestimonialCardContext.Provider\n      value={{\n        carouselRef,\n        api,\n        opts,\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n      }}\n    >\n      <div\n        onKeyDownCapture={handleKeyDown}\n        className={cn('relative', className)}\n        role='region'\n        aria-roledescription='carousel'\n        data-slot='testimonial-card'\n        {...props}\n      >\n        {children}\n      </div>\n    </TestimonialCardContext.Provider>\n  );\n}\n\nfunction TestimonialCardContent({\n  className,\n  ...props\n}: React.ComponentProps<'div'>) {\n  const { carouselRef } = useTestimonialCard();\n  return (\n    <div\n      ref={carouselRef}\n      className='overflow-hidden'\n      data-slot='testimonial-card-content'\n    >\n      <div className={cn('flex', className)} {...props} />\n    </div>\n  );\n}\n\nfunction TestimonialCardItem({\n  className,\n  ...props\n}: React.ComponentProps<'div'>) {\n  return (\n    <div\n      role='group'\n      aria-roledescription='slide'\n      data-slot='testimonial-card-item'\n      className={cn('min-w-0 shrink-0 grow-0 basis-full', className)}\n      {...props}\n    />\n  );\n}\n\nfunction TestimonialCardPrevious({\n  className,\n  variant = 'outline',\n  size = 'icon',\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { scrollPrev, canScrollPrev } = useTestimonialCard();\n  return (\n    <Button\n      data-slot='testimonial-card-previous'\n      variant={variant}\n      size={size}\n      className={cn('absolute size-8 rounded-full', className)}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      {...props}\n    >\n      <ArrowLeft />\n      <span className='sr-only'>Previous slide</span>\n    </Button>\n  );\n}\n\nfunction TestimonialCardNext({\n  className,\n  variant = 'outline',\n  size = 'icon',\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { scrollNext, canScrollNext } = useTestimonialCard();\n  return (\n    <Button\n      data-slot='testimonial-card-next'\n      variant={variant}\n      size={size}\n      className={cn('absolute size-8 rounded-full', className)}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      {...props}\n    >\n      <ArrowRight />\n      <span className='sr-only'>Next slide</span>\n    </Button>\n  );\n}\n\nfunction AnimatedAvatarBorder({\n  children,\n  isActive,\n  progress,\n  borderType = 'solid',\n}: {\n  children: React.ReactNode;\n  isActive: boolean;\n  progress: number;\n  borderType?: 'solid' | 'gradient';\n}) {\n  const offset = isActive ? 301.59 - (301.59 * progress) / 100 : 301.59;\n  const gradientId = React.useId();\n\n  const getStrokeColor = () => {\n    if (borderType === 'gradient') {\n      return `url(#${gradientId})`;\n    }\n    return 'currentColor';\n  };\n\n  return (\n    <div className='relative w-16 h-16 sm:w-20 sm:h-20 shrink-0 text-foreground'>\n      <svg\n        className='absolute inset-0 w-full h-full'\n        viewBox='0 0 100 100'\n        style={{ transform: 'rotate(-90deg)' }}\n      >\n        <circle\n          cx='50'\n          cy='50'\n          r='48'\n          fill='none'\n          stroke={getStrokeColor()}\n          strokeWidth='4'\n          strokeDasharray='301.59'\n          strokeDashoffset={offset}\n          strokeLinecap='round'\n        />\n        {borderType === 'gradient' && (\n          <defs>\n            <linearGradient id={gradientId} x1='0%' y1='0%' x2='100%' y2='100%'>\n              <stop offset='0%' stopColor='#3b82f6' />\n              <stop offset='50%' stopColor='#8b5cf6' />\n              <stop offset='100%' stopColor='#ec4899' />\n            </linearGradient>\n          </defs>\n        )}\n      </svg>\n      <div className='absolute inset-1/2 -translate-x-1/2 -translate-y-1/2 w-14 h-14 sm:w-18 sm:h-18 rounded-full overflow-hidden p-2'>\n        {children}\n      </div>\n    </div>\n  );\n}\n\ntype Testimonial = {\n  description: string;\n  image: string;\n  name: string;\n  handle: string;\n};\n\ntype TestimonialCarouselProps = {\n  data: Testimonial[];\n  borderType?: 'solid' | 'gradient';\n};\n\nexport default function TestimonialCarousel({\n  data,\n  borderType = 'solid',\n}: TestimonialCarouselProps) {\n  const [api, setApi] = React.useState<TestimonialCardApi>();\n  const [current, setCurrent] = React.useState(0);\n  const [progress, setProgress] = React.useState(0);\n\n  React.useEffect(() => {\n    if (!api) return;\n    setCurrent(api.selectedScrollSnap());\n    api.on('select', () => {\n      if (api) setCurrent(api.selectedScrollSnap());\n    });\n  }, [api]);\n\n  React.useEffect(() => {\n    if (!api) return;\n\n    const duration = 5000;\n    const interval = 50;\n    const increment = (interval / duration) * 100;\n    let localProgress = 0;\n\n    const timer = setInterval(() => {\n      localProgress += increment;\n      setProgress(localProgress);\n      if (localProgress >= 100) {\n        if (api) api.scrollNext();\n        localProgress = 0;\n      }\n    }, interval);\n\n    return () => clearInterval(timer);\n  }, [api, current]);\n\n  return (\n    <div className='relative flex items-center justify-center w-full select-none px-2 sm:px-4 md:px-6 py-8'>\n      <TestimonialCard className='relative max-w-4xl w-full' setApi={setApi}>\n        <div className='relative w-full'>\n          <TestimonialCardContent>\n            {data.map((testimonial, index) => (\n              <TestimonialCardItem key={index} className='basis-full'>\n                <Card className='bg-background border h-full'>\n                  <CardContent className='p-4 sm:p-6 md:p-8 h-full flex items-center'>\n                    <div className='flex flex-col sm:flex-row items-start gap-4 sm:gap-6 w-full'>\n                      <AnimatedAvatarBorder\n                        isActive={index === current}\n                        progress={progress}\n                        borderType={borderType}\n                      >\n                        <Avatar className='w-full h-full'>\n                          <AvatarImage\n                            src={testimonial.image}\n                            alt={testimonial.name}\n                          />\n                          <AvatarFallback>\n                            {testimonial.name\n                              .split(' ')\n                              .map((n) => n[0])\n                              .join('')}\n                          </AvatarFallback>\n                        </Avatar>\n                      </AnimatedAvatarBorder>\n                      <div className='flex-1 min-h-30 sm:min-h-35 flex flex-col justify-center'>\n                        <h3 className='text-lg sm:text-xl font-semibold mb-1 text-foreground'>\n                          {testimonial.name}\n                        </h3>\n                        <p className='text-xs sm:text-sm mb-3 sm:mb-4 text-muted-foreground'>\n                          {testimonial.handle}\n                        </p>\n                        <p className='text-sm sm:text-base leading-relaxed text-foreground'>\n                          {testimonial.description}\n                        </p>\n                      </div>\n                    </div>\n                  </CardContent>\n                </Card>\n              </TestimonialCardItem>\n            ))}\n          </TestimonialCardContent>\n        </div>\n\n        <div className='flex items-center justify-center gap-4 mt-6'>\n          <TestimonialCardPrevious className='shadow-lg dark:shadow-gray-800 static!' />\n          <TestimonialCardNext className='shadow-lg dark:shadow-gray-800 static!' />\n        </div>\n      </TestimonialCard>\n    </div>\n  );\n}\n"
    },
    {
      "type": "registry:ui",
      "path": "components/ui/button.tsx",
      "content": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n        success: 'bg-green-600 text-white hover:bg-green-700',\n        warning: 'bg-yellow-500 text-black hover:bg-yellow-600',\n        info: 'bg-blue-500 text-white hover:bg-blue-600',\n        dark: 'bg-gray-800 text-white hover:bg-gray-700',\n        light: 'bg-gray-100 text-gray-800 hover:bg-gray-200',\n        gradient:\n          'bg-linear-to-r from-indigo-500 via-purple-500 to-pink-500 text-white hover:opacity-90',\n        glass:\n          'bg-white/10 backdrop-blur-md text-white border border-white/20 hover:bg-white/20',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'h-9 w-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\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: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: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}"
    }
  ]
}
