spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: client/src/components/ui/carousel.tsx6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import * as React from "react"18import useEmblaCarousel, {19 type UseEmblaCarouselType,20} from "embla-carousel-react"21import { ArrowLeft, ArrowRight } from "lucide-react"2223import { cn } from "@/lib/utils"24import { Button } from "@/components/ui/button"2526type CarouselApi = UseEmblaCarouselType[1]27type UseCarouselParameters = Parameters<typeof useEmblaCarousel>28type CarouselOptions = UseCarouselParameters[0]29type CarouselPlugin = UseCarouselParameters[1]3031type CarouselProps = {32 opts?: CarouselOptions33 plugins?: CarouselPlugin34 orientation?: "horizontal" | "vertical"35 setApi?: (api: CarouselApi) => void36}3738type CarouselContextProps = {39 carouselRef: ReturnType<typeof useEmblaCarousel>[0]40 api: ReturnType<typeof useEmblaCarousel>[1]41 scrollPrev: () => void42 scrollNext: () => void43 canScrollPrev: boolean44 canScrollNext: boolean45} & CarouselProps4647const CarouselContext = React.createContext<CarouselContextProps | null>(null)4849function useCarousel() {50 const context = React.useContext(CarouselContext)5152 if (!context) {53 throw new Error("useCarousel must be used within a <Carousel />")54 }5556 return context57}5859const Carousel = React.forwardRef<60 HTMLDivElement,61 React.HTMLAttributes<HTMLDivElement> & CarouselProps62>(63 (64 {65 orientation = "horizontal",66 opts,67 setApi,68 plugins,69 className,70 children,71 ...props72 },73 ref74 ) => {75 const [carouselRef, api] = useEmblaCarousel(76 {77 ...opts,78 axis: orientation === "horizontal" ? "x" : "y",79 },80 plugins81 )82 const [canScrollPrev, setCanScrollPrev] = React.useState(false)83 const [canScrollNext, setCanScrollNext] = React.useState(false)8485 const onSelect = React.useCallback((api: CarouselApi) => {86 if (!api) {87 return88 }8990 setCanScrollPrev(api.canScrollPrev())91 setCanScrollNext(api.canScrollNext())92 }, [])9394 const scrollPrev = React.useCallback(() => {95 api?.scrollPrev()96 }, [api])9798 const scrollNext = React.useCallback(() => {99 api?.scrollNext()100 }, [api])101102 const handleKeyDown = React.useCallback(103 (event: React.KeyboardEvent<HTMLDivElement>) => {104 if (event.key === "ArrowLeft") {105 event.preventDefault()106 scrollPrev()107 } else if (event.key === "ArrowRight") {108 event.preventDefault()109 scrollNext()110 }111 },112 [scrollPrev, scrollNext]113 )114115 React.useEffect(() => {116 if (!api || !setApi) {117 return118 }119120 setApi(api)121 }, [api, setApi])122123 React.useEffect(() => {124 if (!api) {125 return126 }127128 onSelect(api)129 api.on("reInit", onSelect)130 api.on("select", onSelect)131132 return () => {133 api?.off("select", onSelect)134 }135 }, [api, onSelect])136137 return (138 <CarouselContext.Provider139 value={{140 carouselRef,141 api: api,142 opts,143 orientation:144 orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),145 scrollPrev,146 scrollNext,147 canScrollPrev,148 canScrollNext,149 }}150 >151 <div152 ref={ref}153 onKeyDownCapture={handleKeyDown}154 className={cn("relative", className)}155 role="region"156 aria-roledescription="carousel"157 {...props}158 >159 {children}160 </div>161 </CarouselContext.Provider>162 )163 }164)165Carousel.displayName = "Carousel"166167const CarouselContent = React.forwardRef<168 HTMLDivElement,169 React.HTMLAttributes<HTMLDivElement>170>(({ className, ...props }, ref) => {171 const { carouselRef, orientation } = useCarousel()172173 return (174 <div ref={carouselRef} className="overflow-hidden">175 <div176 ref={ref}177 className={cn(178 "flex",179 orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",180 className181 )}182 {...props}183 />184 </div>185 )186})187CarouselContent.displayName = "CarouselContent"188189const CarouselItem = React.forwardRef<190 HTMLDivElement,191 React.HTMLAttributes<HTMLDivElement>192>(({ className, ...props }, ref) => {193 const { orientation } = useCarousel()194195 return (196 <div197 ref={ref}198 role="group"199 aria-roledescription="slide"200 className={cn(201 "min-w-0 shrink-0 grow-0 basis-full",202 orientation === "horizontal" ? "pl-4" : "pt-4",203 className204 )}205 {...props}206 />207 )208})209CarouselItem.displayName = "CarouselItem"210211const CarouselPrevious = React.forwardRef<212 HTMLButtonElement,213 React.ComponentProps<typeof Button>214>(({ className, variant = "outline", size = "icon", ...props }, ref) => {215 const { orientation, scrollPrev, canScrollPrev } = useCarousel()216217 return (218 <Button219 ref={ref}220 variant={variant}221 size={size}222 className={cn(223 "absolute h-8 w-8 rounded-full",224 orientation === "horizontal"225 ? "-left-12 top-1/2 -translate-y-1/2"226 : "-top-12 left-1/2 -translate-x-1/2 rotate-90",227 className228 )}229 disabled={!canScrollPrev}230 onClick={scrollPrev}231 {...props}232 >233 <ArrowLeft className="h-4 w-4" />234 <span className="sr-only">Previous slide</span>235 </Button>236 )237})238CarouselPrevious.displayName = "CarouselPrevious"239240const CarouselNext = React.forwardRef<241 HTMLButtonElement,242 React.ComponentProps<typeof Button>243>(({ className, variant = "outline", size = "icon", ...props }, ref) => {244 const { orientation, scrollNext, canScrollNext } = useCarousel()245246 return (247 <Button248 ref={ref}249 variant={variant}250 size={size}251 className={cn(252 "absolute h-8 w-8 rounded-full",253 orientation === "horizontal"254 ? "-right-12 top-1/2 -translate-y-1/2"255 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",256 className257 )}258 disabled={!canScrollNext}259 onClick={scrollNext}260 {...props}261 >262 <ArrowRight className="h-4 w-4" />263 <span className="sr-only">Next slide</span>264 </Button>265 )266})267CarouselNext.displayName = "CarouselNext"268269export {270 type CarouselApi,271 Carousel,272 CarouselContent,273 CarouselItem,274 CarouselPrevious,275 CarouselNext,276}277