{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "carousel", "type": "registry:block", "title": "Carousel", "description": "A compound carousel primitive with drag-to-swipe, keyboard navigation, dot/thumbnail indicators, autoplay, and seamless looping.", "author": "Wensity ", "dependencies": [ "@tabler/icons-react", "clsx", "framer-motion", "tailwind-merge" ], "registryDependencies": [], "files": [ { "path": "registry/wensity/lib/utils.ts", "type": "registry:lib", "target": "@lib/utils.ts", "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n" }, { "path": "registry/wensity/carousel.tsx", "type": "registry:component", "target": "@components/wensity/carousel.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n motion,\n AnimatePresence,\n useReducedMotion,\n useAnimationControls,\n type PanInfo,\n} from \"framer-motion\";\nimport {\n IconChevronDown,\n IconChevronLeft,\n IconChevronRight,\n IconChevronUp,\n} from \"@tabler/icons-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ─── Easing ────────────────────────────────────────────────── */\n\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\n\n/* ─── Context ───────────────────────────────────────────────── */\n\ntype CarouselContextValue = {\n carouselId: string;\n orientation: CarouselOrientation;\n index: number;\n count: number;\n loop: boolean;\n direction: number;\n autoplay: boolean;\n autoplayInterval: number;\n select: (next: number) => void;\n next: () => void;\n prev: () => void;\n canPrev: boolean;\n canNext: boolean;\n registerCount: (n: number) => void;\n setIsDragging: (v: boolean) => void;\n};\n\nconst CarouselContext = React.createContext(null);\n\n// Split out from CarouselContext: isHovering/isDragging only affect autoplay\n// progress indicators. Keeping it as its own context means hovering a\n// carousel to pause autoplay only re-renders the progress bar / dots that\n// actually read it, instead of every compound part (content, nav buttons).\nconst CarouselAutoplayPauseContext = React.createContext(false);\n\nfunction useCarouselContext() {\n const ctx = React.useContext(CarouselContext);\n if (!ctx) {\n throw new Error(\"Carousel compound components must be used within \");\n }\n return ctx;\n}\n\nfunction useCarouselAutoplayPaused() {\n return React.useContext(CarouselAutoplayPauseContext);\n}\n\nfunction handleTabRovingKeyDown(\n e: React.KeyboardEvent,\n isVertical: boolean,\n onActivate: (index: number) => void,\n) {\n const tabs = Array.from(\n e.currentTarget.parentElement?.querySelectorAll(\n '[data-slot=\"carousel-tab\"]',\n ) ?? [],\n );\n const idx = tabs.indexOf(e.currentTarget);\n const nextKey = isVertical ? \"ArrowDown\" : \"ArrowRight\";\n const prevKey = isVertical ? \"ArrowUp\" : \"ArrowLeft\";\n\n if (e.key === nextKey) {\n e.preventDefault();\n const next = tabs[(idx + 1) % tabs.length];\n next?.focus();\n onActivate(tabs.indexOf(next));\n } else if (e.key === prevKey) {\n e.preventDefault();\n const prev = tabs[(idx - 1 + tabs.length) % tabs.length];\n prev?.focus();\n onActivate(tabs.indexOf(prev));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n tabs[0]?.focus();\n onActivate(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n tabs[tabs.length - 1]?.focus();\n onActivate(tabs.length - 1);\n }\n}\n\nfunction directionFor(from: number, to: number, count: number, loop: boolean): number {\n if (to === from) return 1;\n if (!loop || count <= 2) return to > from ? 1 : -1;\n const diff = ((to - from) % count + count) % count;\n return diff <= count / 2 ? 1 : -1;\n}\n\n/* ─── Root ──────────────────────────────────────────────────── */\n\nexport interface CarouselProps\n extends Omit, \"onChange\"> {\n index?: number;\n defaultIndex?: number;\n onIndexChange?: (index: number) => void;\n loop?: boolean;\n autoplay?: boolean;\n autoplayInterval?: number;\n pauseOnHover?: boolean;\n orientation?: CarouselOrientation;\n children: React.ReactNode;\n}\n\nexport type CarouselOrientation = \"horizontal\" | \"vertical\";\n\nexport const Carousel = React.forwardRef(\n function Carousel(\n {\n index: indexProp,\n defaultIndex = 0,\n onIndexChange,\n loop = false,\n autoplay = false,\n autoplayInterval = 4000,\n pauseOnHover = true,\n orientation = \"horizontal\",\n className,\n children,\n onKeyDown,\n ...props\n },\n ref,\n ) {\n const carouselId = React.useId();\n const [count, setCount] = React.useState(0);\n const [internalIndex, setInternalIndex] = React.useState(defaultIndex);\n const [direction, setDirection] = React.useState(1);\n const [isHovering, setIsHovering] = React.useState(false);\n const [isFocusWithin, setIsFocusWithin] = React.useState(false);\n const [isDragging, setIsDragging] = React.useState(false);\n const prefersReducedMotion = useReducedMotion();\n\n const isControlled = indexProp !== undefined;\n const index = isControlled ? indexProp : internalIndex;\n\n const select = React.useCallback(\n (next: number, dir?: number) => {\n if (count === 0) return;\n const clamped = loop\n ? ((next % count) + count) % count\n : Math.max(0, Math.min(next, count - 1));\n setDirection(dir ?? directionFor(index, clamped, count, loop));\n if (!isControlled) setInternalIndex(clamped);\n onIndexChange?.(clamped);\n },\n [count, index, loop, isControlled, onIndexChange],\n );\n\n const next = React.useCallback(() => select(index + 1, 1), [select, index]);\n const prev = React.useCallback(() => select(index - 1, -1), [select, index]);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n\n React.useEffect(() => {\n if (count > 0 && index > count - 1) {\n select(count - 1, -1);\n }\n }, [count, index, select]);\n\n // Read the latest index/select through refs inside the autoplay timer so\n // the interval effect below doesn't need `index` as a dependency — a\n // single timer keeps ticking across the whole autoplay session instead\n // of being torn down and recreated on every slide transition.\n const indexRef = React.useRef(index);\n const selectRef = React.useRef(select);\n React.useEffect(() => {\n indexRef.current = index;\n selectRef.current = select;\n });\n\n React.useEffect(() => {\n if (!autoplay || count <= 1) return;\n if (prefersReducedMotion) return;\n if (pauseOnHover && (isHovering || isFocusWithin || isDragging)) return;\n if (!canNext) return;\n const id = setInterval(() => {\n selectRef.current(indexRef.current + 1, 1);\n }, autoplayInterval);\n return () => clearInterval(id);\n }, [\n autoplay,\n autoplayInterval,\n count,\n canNext,\n isHovering,\n isFocusWithin,\n isDragging,\n pauseOnHover,\n prefersReducedMotion,\n ]);\n\n const registerCount = React.useCallback((n: number) => setCount(n), []);\n const isAutoplayPaused =\n Boolean(prefersReducedMotion) ||\n (pauseOnHover && (isHovering || isFocusWithin || isDragging));\n\n const ctx = React.useMemo(\n () => ({\n carouselId,\n orientation,\n index,\n count,\n loop,\n direction,\n autoplay,\n autoplayInterval,\n select,\n next,\n prev,\n canPrev,\n canNext,\n registerCount,\n setIsDragging,\n }),\n [\n carouselId,\n orientation,\n index,\n count,\n loop,\n direction,\n autoplay,\n autoplayInterval,\n select,\n next,\n prev,\n canPrev,\n canNext,\n registerCount,\n ],\n );\n\n return (\n \n \n
setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n onFocusCapture={() => setIsFocusWithin(true)}\n onBlurCapture={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {\n setIsFocusWithin(false);\n }\n }}\n onKeyDown={(e) => {\n const previousKey = orientation === \"vertical\" ? \"ArrowUp\" : \"ArrowLeft\";\n const nextKey = orientation === \"vertical\" ? \"ArrowDown\" : \"ArrowRight\";\n if (e.key === previousKey) {\n e.preventDefault();\n prev();\n } else if (e.key === nextKey) {\n e.preventDefault();\n next();\n }\n onKeyDown?.(e);\n }}\n {...props}\n >\n {children}\n
\n
\n
\n );\n },\n);\n\n/* ─── Item ──────────────────────────────────────────────────── */\n\nexport interface CarouselItemProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\nexport function CarouselItem({ className, children, ...props }: CarouselItemProps) {\n return (\n
\n {children}\n
\n );\n}\n\n/* ─── Content (viewport + drag + directional slide) ───────────── */\n\nconst SWIPE_DISTANCE_THRESHOLD = 60;\nconst SWIPE_VELOCITY_THRESHOLD = 500;\n\nexport interface CarouselContentProps\n extends Omit, \"children\"> {\n children: React.ReactNode;\n draggable?: boolean;\n}\n\nexport function CarouselContent({\n className,\n children,\n draggable = true,\n ...props\n}: CarouselContentProps) {\n const {\n carouselId,\n orientation,\n index,\n count,\n loop,\n direction,\n next,\n prev,\n canPrev,\n canNext,\n setIsDragging,\n registerCount,\n } = useCarouselContext();\n const prefersReducedMotion = useReducedMotion();\n const items = React.useMemo(() => React.Children.toArray(children), [children]);\n\n React.useEffect(() => {\n registerCount(items.length);\n }, [items.length, registerCount]);\n\n const active = items[index];\n\n const slideVariants = {\n enter: (dir: number) => ({\n x: orientation === \"horizontal\" && !prefersReducedMotion ? (dir > 0 ? \"14%\" : \"-14%\") : 0,\n y: orientation === \"vertical\" && !prefersReducedMotion ? (dir > 0 ? \"14%\" : \"-14%\") : 0,\n opacity: 0,\n }),\n center: { x: \"0%\", y: \"0%\", opacity: 1 },\n exit: (dir: number) => ({\n x: orientation === \"horizontal\" && !prefersReducedMotion ? (dir > 0 ? \"-14%\" : \"14%\") : 0,\n y: orientation === \"vertical\" && !prefersReducedMotion ? (dir > 0 ? \"-14%\" : \"14%\") : 0,\n opacity: 0,\n }),\n };\n\n function handleDragEnd(_event: PointerEvent, info: PanInfo) {\n setIsDragging(false);\n const { offset, velocity } = info;\n const dragOffset = orientation === \"vertical\" ? offset.y : offset.x;\n const dragVelocity = orientation === \"vertical\" ? velocity.y : velocity.x;\n const swipedForward =\n dragOffset < -SWIPE_DISTANCE_THRESHOLD || dragVelocity < -SWIPE_VELOCITY_THRESHOLD;\n const swipedBackward =\n dragOffset > SWIPE_DISTANCE_THRESHOLD || dragVelocity > SWIPE_VELOCITY_THRESHOLD;\n if (swipedForward && (canNext || loop)) next();\n else if (swipedBackward && (canPrev || loop)) prev();\n }\n\n return (\n 0 ? `${index + 1} of ${count}` : undefined}\n className={cn(\n // Keep clipping on one composited surface. Repeating the rounded clip\n // on the translated child can expose a one-pixel seam in Chromium.\n \"relative isolate overflow-hidden rounded-[var(--primitive-radius-surface)] transform-gpu\",\n className,\n )}\n {...props}\n >\n \n 1 ? (orientation === \"vertical\" ? \"y\" : \"x\") : false}\n dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}\n dragElastic={prefersReducedMotion ? 0 : 0.65}\n dragTransition={\n prefersReducedMotion\n ? { bounceStiffness: 0, bounceDamping: 1000 }\n : { bounceStiffness: 600, bounceDamping: 40 }\n }\n onDragStart={() => setIsDragging(true)}\n onDragEnd={handleDragEnd}\n className={cn(\n \"h-full\",\n draggable && count > 1 && \"cursor-grab active:cursor-grabbing\",\n )}\n >\n {active}\n \n \n \n );\n}\n\n/* ─── Previous / Next controls ─────────────────────────────── */\n\n/* Outline icon-button recipe (matches Button outline + icon size). */\nconst navButtonClass = cn(\n \"flex size-9 items-center justify-center rounded-[var(--primitive-radius-control)] border outline-none\",\n \"border-[var(--border-strong)] bg-transparent text-[var(--foreground)]\",\n \"transition-colors duration-150\",\n \"hover:border-[color-mix(in_srgb,var(--foreground)_20%,var(--background))] hover:bg-[var(--primitive-surface-hover)]\",\n \"active:border-[color-mix(in_srgb,var(--foreground)_26%,var(--background))] active:bg-[var(--primitive-surface-active)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]\",\n \"disabled:pointer-events-none disabled:opacity-45\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n);\n\nexport interface CarouselControlProps\n extends Omit<\n React.ButtonHTMLAttributes,\n \"children\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"\n > {}\n\nexport function CarouselPrevious({ className, ...props }: CarouselControlProps) {\n const { orientation, prev, canPrev } = useCarouselContext();\n const prefersReducedMotion = useReducedMotion();\n return (\n \n {orientation === \"vertical\" ? (\n \n ) : (\n \n )}\n \n );\n}\n\nexport function CarouselNext({ className, ...props }: CarouselControlProps) {\n const { orientation, next, canNext } = useCarouselContext();\n const prefersReducedMotion = useReducedMotion();\n return (\n \n {orientation === \"vertical\" ? (\n \n ) : (\n \n )}\n \n );\n}\n\n/* ─── Autoplay progress animation (shared by dots + progress bar) ───── */\n\n// Drives a 0→1 `scaleX` fill timed to `autoplayInterval`, resetting whenever\n// `resetKey` changes while `active`, and pausing without resetting when\n// `isAutoplayPaused` is true. Shared by AutoplayDotFill and CarouselProgress\n// so the two indicators can't drift out of sync with each other.\nfunction useAutoplayProgress(\n active: boolean,\n resetKey: unknown,\n autoplayInterval: number,\n isAutoplayPaused: boolean,\n) {\n const prefersReducedMotion = useReducedMotion();\n const controls = useAnimationControls();\n\n React.useEffect(() => {\n if (!active) return;\n controls.set({ scaleX: 0 });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, resetKey, controls]);\n\n React.useEffect(() => {\n if (!active) return;\n if (isAutoplayPaused) {\n controls.stop();\n return;\n }\n controls.start({\n scaleX: 1,\n transition: { duration: prefersReducedMotion ? 0 : autoplayInterval / 1000, ease: \"linear\" },\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, resetKey, isAutoplayPaused, autoplayInterval, controls, prefersReducedMotion]);\n\n return controls;\n}\n\n/* ─── Dots ──────────────────────────────────────────────────── */\n\n// When autoplay is on, only the active indicator expands into a progress\n// track. Completed slides return to solid dots; upcoming slides stay muted.\nfunction AutoplayDotFill({ isActive, isComplete }: { isActive: boolean; isComplete: boolean }) {\n const { autoplayInterval } = useCarouselContext();\n const isAutoplayPaused = useCarouselAutoplayPaused();\n const controls = useAutoplayProgress(isActive, isActive, autoplayInterval, isAutoplayPaused);\n\n if (isActive) {\n return (\n \n \n \n );\n }\n\n return (\n \n );\n}\n\nexport interface CarouselDotsProps extends React.HTMLAttributes {}\n\nexport function CarouselDots({ className, ...props }: CarouselDotsProps) {\n const { carouselId, orientation, index, count, autoplay, select } = useCarouselContext();\n const isVertical = orientation === \"vertical\";\n const prefersReducedMotion = useReducedMotion();\n\n if (count <= 1) return null;\n\n const dotButtonClass =\n \"flex outline-none rounded-full focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]\";\n\n if (autoplay) {\n return (\n \n {Array.from({ length: count }, (_, i) => (\n select(i)}\n onKeyDown={(e) => handleTabRovingKeyDown(e, isVertical, select)}\n className={dotButtonClass}\n >\n \n \n ))}\n \n );\n }\n\n return (\n \n {Array.from({ length: count }, (_, i) => {\n const isActive = i === index;\n return (\n select(i)}\n onKeyDown={(e) => handleTabRovingKeyDown(e, isVertical, select)}\n className={cn(\"relative flex h-4 w-4 items-center justify-center\", dotButtonClass)}\n >\n \n {isActive && (\n \n )}\n \n \n );\n })}\n \n );\n}\n\n/* ─── Thumbnails ────────────────────────────────────────────── */\n\nexport interface CarouselThumbnailsProps\n extends Omit, \"children\"> {\n children: React.ReactNode;\n}\n\nexport function CarouselThumbnails({ className, children, ...props }: CarouselThumbnailsProps) {\n const { orientation, index, select } = useCarouselContext();\n const isVertical = orientation === \"vertical\";\n const items = React.Children.toArray(children);\n\n return (\n \n {items.map((child, i) => {\n const isActive = i === index;\n return (\n select(i)}\n onKeyDown={(e) => handleTabRovingKeyDown(e, isVertical, select)}\n className={cn(\n \"relative shrink-0 overflow-hidden rounded-[var(--primitive-radius-control-sm)] border outline-none transition-colors duration-150\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]\",\n isActive\n ? \"border-[var(--primitive-control-solid)] ring-1 ring-[var(--primitive-control-solid)]\"\n : \"border-[var(--border)] opacity-60 hover:opacity-90 hover:border-[var(--border-strong)] hover:bg-[var(--primitive-surface-hover)]\",\n )}\n >\n {child}\n \n );\n })}\n \n );\n}\n\n/* ─── Progress (autoplay timing indicator) ─────────────────── */\n\nexport interface CarouselProgressProps extends React.HTMLAttributes {}\n\nexport function CarouselProgress({ className, ...props }: CarouselProgressProps) {\n const { index, count, autoplay, autoplayInterval } = useCarouselContext();\n const isAutoplayPaused = useCarouselAutoplayPaused();\n const active = autoplay && count > 1;\n const controls = useAutoplayProgress(active, index, autoplayInterval, isAutoplayPaused);\n\n if (!active) return null;\n\n return (\n \n \n \n );\n}\n" } ], "cssVars": { "theme": { "font-sans": "var(--font-satoshi, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif)", "font-display": "var(--font-cabinet, ui-sans-serif, system-ui, sans-serif)", "color-background": "var(--background)", "color-foreground": "var(--foreground)", "color-surface": "var(--surface)", "color-surface-muted": "var(--surface-muted)", "color-border": "var(--border)", "color-border-strong": "var(--border-strong)", "color-muted": "var(--muted)", "color-muted-foreground": "var(--muted-foreground)", "color-ring": "var(--ring)", "color-chili-50": "#fff1ee", "color-chili-100": "#ffe2db", "color-chili-200": "#ffa896", "color-chili-300": "#ff8a73", "color-chili-400": "#f15a45", "color-chili-500": "#cd1c18", "color-chili-600": "#b31614", "color-chili-700": "#9b1313", "color-chili-800": "#6a0d0e", "color-chili-900": "#38000a", "color-chili-950": "#1f0006", "color-primitive-surface-elevated": "var(--primitive-surface-elevated)", "color-primitive-surface-overlay": "var(--primitive-surface-overlay)", "color-primitive-surface-hover": "var(--primitive-surface-hover)", "color-primitive-surface-active": "var(--primitive-surface-active)", "color-primitive-surface-selected": "var(--primitive-surface-selected)", "color-primitive-border-subtle": "var(--primitive-border-subtle)", "color-primitive-ring": "var(--primitive-ring)", "color-primitive-text-secondary": "var(--primitive-text-secondary)", "color-primitive-text-placeholder": "var(--primitive-text-placeholder)", "color-primitive-text-inverse": "var(--primitive-text-inverse)", "color-primitive-destructive": "var(--primitive-destructive)", "color-primitive-destructive-hover": "var(--primitive-destructive-hover)", "color-primitive-destructive-active": "var(--primitive-destructive-active)", "color-primitive-destructive-foreground": "var(--primitive-destructive-foreground)", "color-primitive-destructive-surface": "var(--primitive-destructive-surface)", "color-primitive-destructive-border": "var(--primitive-destructive-border)", "color-primitive-success": "var(--primitive-success)", "color-primitive-warning": "var(--primitive-warning)", "color-primitive-info": "var(--primitive-info)", "color-primitive-control-solid": "var(--primitive-control-solid)", "color-primitive-control-solid-hover": "var(--primitive-control-solid-hover)", "color-primitive-control-solid-active": "var(--primitive-control-solid-active)", "color-primitive-control-solid-foreground": "var(--primitive-control-solid-foreground)", "color-primitive-chart-1": "var(--primitive-chart-1)", "color-primitive-chart-2": "var(--primitive-chart-2)", "color-primitive-chart-3": "var(--primitive-chart-3)", "color-primitive-chart-4": "var(--primitive-chart-4)", "color-primitive-chart-5": "var(--primitive-chart-5)", "color-primitive-chart-6": "var(--primitive-chart-6)", "radius-primitive": "var(--primitive-radius)", "radius-primitive-control": "var(--primitive-radius-control)", "radius-primitive-control-sm": "var(--primitive-radius-control-sm)", "radius-primitive-surface": "var(--primitive-radius-surface)", "radius-primitive-item": "var(--primitive-radius-item)", "font-primitive-sans": "var(--primitive-font-sans)", "font-primitive-display": "var(--primitive-font-display)", "font-primitive-mono": "var(--primitive-font-mono)", "spacing-primitive-control-height-sm": "var(--primitive-control-height-sm)", "spacing-primitive-control-height-md": "var(--primitive-control-height-md)", "spacing-primitive-control-height-lg": "var(--primitive-control-height-lg)" }, "light": { "background": "#fafafa", "foreground": "#0a0a0a", "surface": "#ffffff", "surface-muted": "#f4f4f5", "border": "rgba(10, 10, 10, 0.08)", "border-strong": "rgba(10, 10, 10, 0.16)", "muted": "#f4f4f5", "muted-foreground": "#52525b", "ring": "#cd1c18", "pattern-fg": "rgba(10, 10, 10, 0.07)", "llb-primary": "#18181b", "llb-primary-fg": "#ffffff", "llb-success": "#1f883d", "llb-error": "#cf222e", "primitive-surface-elevated": "#ffffff", "primitive-surface-overlay": "#ffffff", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 4%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 7%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 6%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.875rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#24292d", "primitive-control-solid-hover": "#2c3237", "primitive-control-solid-active": "#1e2326", "primitive-control-solid-foreground": "#ffffff", "primitive-chart-1": "#2a78d6", "primitive-chart-2": "#1baf7a", "primitive-chart-3": "#eda100", "primitive-chart-4": "#008300", "primitive-chart-5": "#4a3aa7", "primitive-chart-6": "#e34948", "primitive-text-inverse": "#ffffff", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#dc2626", "primitive-destructive-hover": "#b91c1c", "primitive-destructive-active": "#991b1b", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(220, 38, 38, 0.10)", "primitive-destructive-border": "rgba(220, 38, 38, 0.45)", "primitive-success": "#047857", "primitive-success-surface": "rgba(4, 120, 87, 0.10)", "primitive-success-border": "rgba(4, 120, 87, 0.45)", "primitive-warning": "#b45309", "primitive-warning-surface": "rgba(180, 83, 9, 0.10)", "primitive-warning-border": "rgba(180, 83, 9, 0.45)", "primitive-info": "#0369a1", "primitive-info-surface": "rgba(3, 105, 161, 0.10)", "primitive-info-border": "rgba(3, 105, 161, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "max(0px, calc(var(--primitive-radius) - 4px))", "primitive-radius-surface": "calc(var(--primitive-radius) + min(2px, var(--primitive-radius)))", "primitive-radius-item": "max(0px, calc(var(--primitive-radius-surface) - 6px))", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 24px -12px rgba(0, 0, 0, 0.08)", "primitive-shadow-overlay": "0 1px 2px rgba(0, 0, 0, 0.06), 0 18px 48px -24px rgba(0, 0, 0, 0.35)", "primitive-shadow-modal": "0 1px 2px rgba(0, 0, 0, 0.08), 0 24px 64px -28px rgba(0, 0, 0, 0.42)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" }, "dark": { "background": "#0a0a0b", "foreground": "#f5f5f6", "surface": "#111113", "surface-muted": "#18181b", "border": "rgba(255, 255, 255, 0.08)", "border-strong": "rgba(255, 255, 255, 0.14)", "muted": "#1c1c1f", "muted-foreground": "#a1a1aa", "ring": "#cd1c18", "pattern-fg": "rgba(255, 255, 255, 0.06)", "llb-primary": "#f5f5f6", "llb-primary-fg": "#0a0a0b", "llb-success": "#2da44e", "llb-error": "#f85149", "primitive-surface-elevated": "#0f0f10", "primitive-surface-overlay": "#141415", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 5%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 10%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 8%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.875rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#f5f3ee", "primitive-control-solid-hover": "#ffffff", "primitive-control-solid-active": "#e8e4da", "primitive-control-solid-foreground": "#151719", "primitive-chart-1": "#3987e5", "primitive-chart-2": "#199e70", "primitive-chart-3": "#c98500", "primitive-chart-4": "#008300", "primitive-chart-5": "#9085e9", "primitive-chart-6": "#e66767", "primitive-text-inverse": "#151719", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#e11d2e", "primitive-destructive-hover": "#c1121f", "primitive-destructive-active": "#a4161a", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(225, 29, 46, 0.12)", "primitive-destructive-border": "rgba(225, 29, 46, 0.52)", "primitive-success": "#34d399", "primitive-success-surface": "rgba(52, 211, 153, 0.12)", "primitive-success-border": "rgba(52, 211, 153, 0.45)", "primitive-warning": "#fbbf24", "primitive-warning-surface": "rgba(251, 191, 36, 0.12)", "primitive-warning-border": "rgba(251, 191, 36, 0.45)", "primitive-info": "#38bdf8", "primitive-info-surface": "rgba(56, 189, 248, 0.12)", "primitive-info-border": "rgba(56, 189, 248, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "max(0px, calc(var(--primitive-radius) - 4px))", "primitive-radius-surface": "calc(var(--primitive-radius) + min(2px, var(--primitive-radius)))", "primitive-radius-item": "max(0px, calc(var(--primitive-radius-surface) - 6px))", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -12px rgba(0, 0, 0, 0.6)", "primitive-shadow-overlay": "0 1px 2px rgba(0, 0, 0, 0.4), 0 20px 56px -28px rgba(0, 0, 0, 0.72)", "primitive-shadow-modal": "0 1px 2px rgba(0, 0, 0, 0.45), 0 28px 72px -32px rgba(0, 0, 0, 0.8)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" } }, "css": { "@layer base": { ":where([data-wensity-primitive])": { "font-family": "var(--primitive-font-sans)" } }, "@utility scrollbar-hidden": { "scrollbar-width": "none", "-ms-overflow-style": "none", "&::-webkit-scrollbar": { "display": "none" } }, "@keyframes wensity-marquee-x": { "from": { "transform": "translate3d(0, 0, 0)" }, "to": { "transform": "translate3d(-50%, 0, 0)" } }, "@keyframes wensity-marquee-x-reverse": { "from": { "transform": "translate3d(-50%, 0, 0)" }, "to": { "transform": "translate3d(0, 0, 0)" } }, "@keyframes wensity-morph-rot-cw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(360deg)" } }, "@keyframes wensity-morph-rot-ccw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(-360deg)" } } }, "docs": "Free Wensity component. Installs to @components/wensity/carousel.tsx. For Pro components and updates, use pnpm dlx wensity@latest add carousel.", "categories": [ "UI Primitives", "wensity", "free" ], "meta": { "wensity": { "slug": "carousel", "access": "free", "category": "UI Primitives", "kind": "component", "componentUrl": "https://ui.wensity.com/primitives/carousel", "shadcnUrl": "https://ui.wensity.com/r/carousel", "cliInstall": "pnpm dlx wensity@latest add carousel" } } }