Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions components/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ export default function TopNav() {
target="_blank"
rel="noopener noreferrer"
>
<Button variant="secondary" size="sm">
<GithubIcon size="14" className="mr-2" />
Star on GitHub
<Button variant="secondary" size="icon">
<GithubIcon size="14"/>
</Button>
</Link>
<Button variant="secondary" size="icon" onClick={toggleTheme}>
Expand Down
241 changes: 241 additions & 0 deletions components/retroui/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"use client"

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

import { cn } from "@/lib/utils"
import { Button } from "@/components/retroui/Button"

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",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()

return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}

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

return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}

const CarouselObject = Object.assign(Carousel, {
Content: CarouselContent,
Item: CarouselItem,
Previous: CarouselPrevious,
Next: CarouselNext,
})

export { CarouselObject as Carousel }
19 changes: 19 additions & 0 deletions config/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export const componentConfig: {
name: "barChart",
filePath: "components/retroui/charts/BarChart.tsx",
},
carousel: {
name: "carousel",
filePath: "components/retroui/Carousel.tsx",
},
checkbox: {
name: "checkbox",
filePath: "components/retroui/Checkbox.tsx",
Expand Down Expand Up @@ -295,6 +299,21 @@ export const componentConfig: {
filePath: "preview/components/calendar-style-default.tsx",
preview: lazy(() => import("@/preview/components/calendar-style-default")),
},
"carousel-style-default": {
name: "carousel-style-default",
filePath: "preview/components/carousel-style-default.tsx",
preview: lazy(() => import("@/preview/components/carousel-style-default")),
},
"carousel-style-sizes": {
name: "carousel-style-sizes",
filePath: "preview/components/carousel-style-sizes.tsx",
preview: lazy(() => import("@/preview/components/carousel-style-sizes")),
},
"carousel-style-vertical": {
name: "carousel-style-vertical",
filePath: "preview/components/carousel-style-vertical.tsx",
preview: lazy(() => import("@/preview/components/carousel-style-vertical")),
},
"card-style-default": {
name: "card-style-default",
filePath: "preview/components/card-style-default.tsx",
Expand Down
1 change: 1 addition & 0 deletions config/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const navConfig: INavigationConfig = {
{ title: "Button", href: `${componentsRoute}/button` },
{ title: "Card", href: `${componentsRoute}/card` },
{ title: "Calendar", href: `${componentsRoute}/calendar`, tag: "New" },
{ title: "Carousel", href: `${componentsRoute}/carousel`, tag: "New" },
{ title: "Checkbox", href: `${componentsRoute}/checkbox` },
{ title: "Command", href: `${componentsRoute}/command` },
{ title: "Dialog", href: `${componentsRoute}/dialog` },
Expand Down
54 changes: 54 additions & 0 deletions content/docs/components/carousel.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Carousel
description: Let your users select a date to cancel subscription.
lastUpdated: 14 Nov, 2025
---

<ComponentShowcase name="carousel-style-default" />
<br />
<br />

## Installation

<ComponentInstall>
<ComponentInstall.Cli npmCommand="npx shadcn@latest add @retroui/carousel" />
<ComponentInstall.Manual>

#### 1. Install dependencies:

```sh
npm install react-day-picker lucide-react
```

<br />

#### 2. Copy the code 👇 into your project:

<ComponentSource name="carousel" />

</ComponentInstall.Manual>
</ComponentInstall>

<br />
<br />

## Examples

### Default

<ComponentShowcase name="carousel-style-default" />

<br />
<br />

### Sizes

<ComponentShowcase name="carousel-style-sizes" />

<br />
<br />

### Vertical

<ComponentShowcase name="carousel-style-vertical" />

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"cmdk": "^1.1.1",
"contentlayer": "^0.3.4",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.445.0",
"mdast-util-toc": "^7.1.0",
"next": "14.2.7",
Expand Down
Loading