# Tweet Carousel

> A swipeable carousel of tweet cards with author, timestamp, and text. Rotate through recent posts or testimonials without stacking them down the page.

A shadcn/ui block from the [@shadcn-ui-blocks](https://www.shadcn-ui-blocks.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-ui-blocks/portfolio-portfolio-social-media-embeds-tweet-carousel).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-ui-blocks/portfolio-portfolio-social-media-embeds-tweet-carousel
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-ui-blocks/portfolio-portfolio-social-media-embeds-tweet-carousel-light-37d9f605b300d658.png
- Registry item: https://www.shadcn-ui-blocks.com/r/portfolio-portfolio-social-media-embeds-tweet-carousel.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.shadcn-ui-blocks.com/r/portfolio-portfolio-social-media-embeds-tweet-carousel.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `class-variance-authority`, `embla-carousel-react`, `lucide-react`, `radix-ui`

## Files

### `components/blocks/portfolio/portfolio-social-media-embeds/tweet-carousel.tsx`

```tsx
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { ChevronLeft, ChevronRight, MessageCircle, Repeat2, Heart, ExternalLink, VerifiedIcon } from 'lucide-react';
import { Twitter } from '@/components/ui/brand-icons';

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  type CarouselApi,
} from '@/components/ui/carousel';
import { useState } from 'react';

// Sample tweets data
const tweets = [
  {
    id: '1',
    user: {
      name: 'Alex Morgan',
      handle: '@alexmdev',
      avatar:
        'https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=1170&auto=format&fit=crop',
      verified: true,
    },
    content:
      'Just launched my new portfolio website built with Next.js and Tailwind CSS! Check it out and let me know what you think. #webdev #frontend #portfoliowebsite',
    date: '2h ago',
    metrics: {
      replies: 12,
      retweets: 24,
      likes: 89,
    },
    url: '#',
  },
  {
    id: '2',
    user: {
      name: 'Sarah Johnson',
      handle: '@sarahj_ux',
      avatar:
        'https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=1287&auto=format&fit=crop',
      verified: true,
    },
    content:
      "Had the pleasure of working with @alexmdev on our company's website redesign. His technical skills and eye for design are exceptional! Highly recommend for any web development project. 💯",
    date: '1d ago',
    metrics: {
      replies: 3,
      retweets: 15,
      likes: 67,
    },
    url: '#',
  },
  {
    id: '3',
    user: {
      name: 'Tech Insights',
      handle: '@techinsights',
      avatar:
        'https://images.unsplash.com/photo-1628155930542-3c7a64e2c833?q=80&w=1074&auto=format&fit=crop',
      verified: true,
    },
    content:
      'We interviewed @alexmdev about his approach to modern web development in our latest podcast episode. Great insights on balancing performance and aesthetics! Listen now: techinsights.com/podcast/ep45',
    date: '3d ago',
    metrics: {
      replies: 18,
      retweets: 42,
      likes: 136,
    },
    url: '#',
  },
  {
    id: '4',
    user: {
      name: 'Alex Morgan',
      handle: '@alexmdev',
      avatar:
        'https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=1170&auto=format&fit=crop',
      verified: true,
    },
    content:
      "Excited to announce that I'm joining @innovatetech as a Senior Frontend Developer next month! Looking forward to building amazing web experiences with this talented team. #careerchange #frontenddeveloper",
    date: '5d ago',
    metrics: {
      replies: 32,
      retweets: 18,
      likes: 215,
    },
    url: '#',
  },
  {
    id: '5',
    user: {
      name: 'WebDevConf',
      handle: '@webdevconf',
      avatar:
        'https://images.unsplash.com/photo-1540575467063-178a50c2df87?q=80&w=1170&auto=format&fit=crop',
      verified: true,
    },
    content:
      "Thank you @alexmdev for your insightful presentation on 'Building Accessible Web Applications' at #WebDevConf2023! Attendees loved the practical demonstrations and code examples.",
    date: '1w ago',
    metrics: {
      replies: 8,
      retweets: 27,
      likes: 103,
    },
    url: '#',
  },
  {
    id: '6',
    user: {
      name: 'CodeMentor',
      handle: '@codementor',
      avatar:
        'https://images.unsplash.com/photo-1521119989659-a83eee488004?q=80&w=1123&auto=format&fit=crop',
      verified: false,
    },
    content:
      "Our latest 'Developer Spotlight' features @alexmdev and his journey from self-taught programmer to senior developer. Read about his learning strategies and career tips: codementor.io/spotlight/alex-morgan",
    date: '2w ago',
    metrics: {
      replies: 5,
      retweets: 14,
      likes: 76,
    },
    url: '#',
  },
];

export default function TweetCarousel() {
  const [api, setApi] = useState<CarouselApi>();

  return (
    <div className="bg-background py-16 md:py-24">
      <div className="container mx-auto px-4 2xl:max-w-[1400px]">
        <div className="mb-10 text-center">
          <div className="mb-3 inline-flex items-center gap-2">
            <Twitter className="h-5 w-5 text-sky-500" />
            <Badge
              variant="outline"
              className="border-sky-200 bg-sky-50 px-3 py-1 text-xs font-medium tracking-wide text-sky-600 uppercase dark:border-sky-500/20 dark:bg-sky-500/10 dark:text-sky-400"
            >
              Twitter
            </Badge>
          </div>
          <h2 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
            Latest Tweets
          </h2>
          <p className="text-muted-foreground mx-auto mt-4 max-w-2xl">
            Stay updated with my latest thoughts and interactions in the tech
            community
          </p>
        </div>

        {/* Tweet Carousel with Navigation */}
        <div className="relative">
          <Carousel
            opts={{
              align: 'start',
              loop: true,
            }}
            className="w-full"
            setApi={setApi}
          >
            <CarouselContent className="pt-2 pb-6">
              {tweets.map((tweet) => (
                <CarouselItem
                  key={tweet.id}
                  className="md:basis-1/2 lg:basis-1/3"
                >
                  <Card className="h-full border shadow-sm transition-all hover:shadow-md">
                    <CardContent className="flex h-full flex-col">
                      {/* Tweet Header */}
                      <div className="mb-4 flex items-start justify-between">
                        <div className="flex items-center gap-3">
                          <div className="relative h-10 w-10 overflow-hidden rounded-full">
                            <img
                              src={tweet.user.avatar}
                              alt={tweet.user.name}
                              className="object-cover"
                            />
                          </div>
                          <div>
                            <div className="flex items-center gap-1">
                              <span className="text-sm font-semibold">
                                {tweet.user.name}
                              </span>
                              {tweet.user.verified && (
                                <VerifiedIcon
                                  className="h-3.5 w-3.5 text-sky-500"
                                  fill="#0ea5e9"
                                />
                              )}
                            </div>
                            <div className="text-muted-foreground text-xs">
                              {tweet.user.handle}
                            </div>
                          </div>
                        </div>
                        <Twitter className="h-5 w-5 text-sky-500" />
                      </div>

                      {/* Tweet Content */}
                      <p className="mb-4 grow text-sm">{tweet.content}</p>

                      {/* Tweet Date and Metrics */}
                      <div className="border-t pt-4">
                        <div className="text-muted-foreground mb-2 text-xs">
                          {tweet.date}
                        </div>
                        <div className="flex items-center justify-between">
                          <div className="flex items-center gap-4">
                            <div className="text-muted-foreground flex items-center gap-1 hover:text-sky-500">
                              <MessageCircle className="h-4 w-4" />
                              <span className="text-xs">
                                {tweet.metrics.replies}
                              </span>
                            </div>
                            <div className="text-muted-foreground flex items-center gap-1 hover:text-green-500">
                              <Repeat2 className="h-4 w-4" />
                              <span className="text-xs">
                                {tweet.metrics.retweets}
                              </span>
                            </div>
                            <div className="text-muted-foreground flex items-center gap-1 hover:text-red-500">
                              <Heart className="h-4 w-4" />
                              <span className="text-xs">
                                {tweet.metrics.likes}
                              </span>
                            </div>
                          </div>
                          <Button
                            variant="ghost"
                            size="icon"
                            className="h-8 w-8 rounded-full hover:bg-sky-50 hover:text-sky-500 dark:hover:bg-sky-950/50"
                            asChild
                          >
                            <a
                              href={tweet.url}
                              target="_blank"
                              rel="noopener noreferrer"
                            >
                              <ExternalLink className="h-4 w-4" />
                            </a>
                          </Button>
                        </div>
                      </div>
                    </CardContent>
                  </Card>
                </CarouselItem>
              ))}
            </CarouselContent>
          </Carousel>
        </div>

        {/* Footer with Mobile Navigation and Link */}
        <div className="flex flex-col items-center space-y-6">
          <div className="flex items-center justify-center gap-4">
            <Button
              variant="outline"
              size="sm"
              className="h-9 w-9 rounded-full p-0"
              onClick={() => api?.scrollPrev()}
            >
              <ChevronLeft className="h-5 w-5" />
            </Button>
            <Button
              variant="outline"
              size="sm"
              className="h-9 w-9 rounded-full p-0"
              onClick={() => api?.scrollNext()}
            >
              <ChevronRight className="h-5 w-5" />
            </Button>
          </div>

          <Button
            variant="outline"
            className="gap-2 border-sky-200 hover:bg-sky-50 hover:text-sky-600 dark:border-sky-500/20 dark:hover:bg-sky-950/30 dark:hover:text-sky-400"
            asChild
          >
            <a href="#" target="_blank" rel="noopener noreferrer">
              <Twitter className="h-4 w-4" />
              <span>Follow me on Twitter</span>
            </a>
          </Button>
        </div>
      </div>
    </div>
  );
}
```

### `components/ui/badge.tsx`

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
        secondary:
          "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
        destructive:
          "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
        outline:
          "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
        ghost:
          "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function Badge({
  className,
  variant = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"span"> &
  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot.Root : "span"

  return (
    <Comp
      data-slot="badge"
      data-variant={variant}
      className={cn(badgeVariants({ variant }), className)}
      {...props}
    />
  )
}

export { Badge, badgeVariants }
```

### `components/ui/button.tsx`

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        icon: "size-9",
        "icon-xs":
          "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
        "icon-lg": "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Button({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot.Root : "button"

  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }
```

### `components/ui/card.tsx`

```tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Card({
  className,
  size = "default",
  ...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
  return (
    <div
      data-slot="card"
      data-size={size}
      className={cn(
        "group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
        className
      )}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
        className
      )}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-title"
      className={cn(
        "font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
        className
      )}
      {...props}
    />
  )
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-action"
      className={cn(
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
        className
      )}
      {...props}
    />
  )
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
      {...props}
    />
  )
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn(
        "flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
        className
      )}
      {...props}
    />
  )
}

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardContent,
}
```

### `components/ui/brand-icons.tsx`

```tsx
import type { ReactNode, SVGProps } from "react"

type BrandIconProps = Omit<SVGProps<SVGSVGElement>, "color"> & {
  title?: string
  color?: string
  size?: number | string
}

/**
 * The brand glyphs Simple Icons has dropped at their owners' request. Every
 * other brand comes straight from the package, imported where it is used
 * (`import { SiGithub } from "@icons-pack/react-simple-icons"`); these three
 * keep the last path Simple Icons published, on its 24 unit grid, and take the
 * package's props (title, color, size) so both read alike in a block.
 */
function BrandSvg({
  children,
  color = "currentColor",
  size = 24,
  title,
  ...props
}: BrandIconProps & { children: ReactNode }) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width={size}
      height={size}
      fill={color}
      viewBox="0 0 24 24"
      {...props}
    >
      <title>{title}</title>
      {children}
    </svg>
  )
}

function Linkedin(props: BrandIconProps) {
  return (
    <BrandSvg title="LinkedIn" {...props}>
      <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
    </BrandSvg>
  )
}

function Slack(props: BrandIconProps) {
  return (
    <BrandSvg title="Slack" {...props}>
      <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" />
    </BrandSvg>
  )
}

function Twitter(props: BrandIconProps) {
  return (
    <BrandSvg title="Twitter" {...props}>
      <path d="M23.4 4.8c-.8.4-1.7.6-2.6.7.9-.6 1.6-1.5 2-2.5-.9.5-1.9.9-2.9 1.1a4.5 4.5 0 0 0-7.8 4.1A12.8 12.8 0 0 1 2.8 3.5a4.5 4.5 0 0 0 1.4 6.1c-.7 0-1.4-.2-2-.6v.1c0 2.2 1.6 4.1 3.6 4.5-.4.1-.8.2-1.2.2-.3 0-.6 0-.9-.1.6 1.8 2.2 3.1 4.2 3.1a9.1 9.1 0 0 1-5.6 1.9c-.4 0-.7 0-1.1-.1a12.8 12.8 0 0 0 7 2c8.3 0 12.9-6.9 12.9-12.9v-.6c.9-.6 1.7-1.4 2.3-2.3Z" />
    </BrandSvg>
  )
}

export { Linkedin, Slack, Twitter }
```

### `components/ui/carousel.tsx`

```tsx
"use client"

import * as React from "react"
import useEmblaCarousel, {
  type UseEmblaCarouselType,
} from "embla-carousel-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"

type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]

type CarouselProps = {
  opts?: CarouselOptions
  plugins?: CarouselPlugin
  orientation?: "horizontal" | "vertical"
  setApi?: (api: CarouselApi) => void
}

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0]
  api: ReturnType<typeof useEmblaCarousel>[1]
  scrollPrev: () => void
  scrollNext: () => void
  canScrollPrev: boolean
  canScrollNext: boolean
} & CarouselProps

const CarouselContext = React.createContext<CarouselContextProps | null>(null)

function useCarousel() {
  const context = React.useContext(CarouselContext)

  if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />")
  }

  return context
}

function Carousel({
  orientation = "horizontal",
  opts,
  setApi,
  plugins,
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & CarouselProps) {
  const [carouselRef, api] = useEmblaCarousel(
    {
      ...opts,
      axis: orientation === "horizontal" ? "x" : "y",
    },
    plugins
  )
  const [canScrollPrev, setCanScrollPrev] = React.useState(false)
  const [canScrollNext, setCanScrollNext] = React.useState(false)

  const onSelect = React.useCallback((api: CarouselApi) => {
    if (!api) return
    setCanScrollPrev(api.canScrollPrev())
    setCanScrollNext(api.canScrollNext())
  }, [])

  const scrollPrev = React.useCallback(() => {
    api?.scrollPrev()
  }, [api])

  const scrollNext = React.useCallback(() => {
    api?.scrollNext()
  }, [api])

  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "ArrowLeft") {
        event.preventDefault()
        scrollPrev()
      } else if (event.key === "ArrowRight") {
        event.preventDefault()
        scrollNext()
      }
    },
    [scrollPrev, scrollNext]
  )

  React.useEffect(() => {
    if (!api || !setApi) return
    setApi(api)
  }, [api, setApi])

  React.useEffect(() => {
    if (!api) return
    onSelect(api)
    api.on("reInit", onSelect)
    api.on("select", onSelect)

    return () => {
      api?.off("select", onSelect)
    }
  }, [api, onSelect])

  return (
    <CarouselContext.Provider
      value={{
        carouselRef,
        api: api,
        opts,
        orientation:
          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
        scrollPrev,
        scrollNext,
        canScrollPrev,
        canScrollNext,
      }}
    >
      <div
        onKeyDownCapture={handleKeyDown}
        className={cn("relative", className)}
        role="region"
        aria-roledescription="carousel"
        data-slot="carousel"
        {...props}
      >
        {children}
      </div>
    </CarouselContext.Provider>
  )
}

function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
  const { carouselRef, orientation } = useCarousel()

  return (
    <div
      ref={carouselRef}
      className="overflow-hidden"
      data-slot="carousel-content"
    >
      <div
        className={cn(
          "flex",
          orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
          className
        )}
        {...props}
      />
    </div>
  )
}

function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
  const { orientation } = useCarousel()

  return (
    <div
      role="group"
      aria-roledescription="slide"
      data-slot="carousel-item"
      className={cn(
        "min-w-0 shrink-0 grow-0 basis-full",
        orientation === "horizontal" ? "pl-4" : "pt-4",
        className
      )}
      {...props}
    />
  )
}

function CarouselPrevious({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel()

  return (
    <Button
      data-slot="carousel-previous"
      variant={variant}
      size={size}
      className={cn(
        "absolute touch-manipulation rounded-full",
        orientation === "horizontal"
          ? "top-1/2 -left-12 -translate-y-1/2"
          : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
        className
      )}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      {...props}
    >
      <ChevronLeftIcon />
      <span className="sr-only">Previous slide</span>
    </Button>
  )
}

function CarouselNext({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollNext, canScrollNext } = useCarousel()

  return (
    <Button
      data-slot="carousel-next"
      variant={variant}
      size={size}
      className={cn(
        "absolute touch-manipulation rounded-full",
        orientation === "horizontal"
          ? "top-1/2 -right-12 -translate-y-1/2"
          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
        className
      )}
      disabled={!canScrollNext}
      onClick={scrollNext}
      {...props}
    >
      <ChevronRightIcon />
      <span className="sr-only">Next slide</span>
    </Button>
  )
}

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  useCarousel,
}
```
