Files
shadcn/app/registry/default/ui/apple-cards-carousel.tsx
zeekay e0ec477433 feat: full docs modernization — zero 404s, 585 pages, dep updates
- Enable all 15 finance component docs (from .disabled → .mdx)
- Create stock-market.mdx, 8 chart docs, 8 block docs, 2 framework docs
- Add direction.tsx component (RTL/LTR support from upstream shadcn)
- Add xs/icon-xs button sizes, data-variant/data-size attributes
- Fix build-registry.mts to handle default exports in namespace re-exports
- Add default props to order-entry, positions-list, orders-history, trading-panel
- Fix 18 navigation stubs → re-export from root implementations
- Add 44 orphaned MDX files to nav (installation, dark-mode, desktop, guides, etc)
- Migrate 6 framer-motion imports → motion/react
- Bump React 19.2.0→19.2.4, Next.js 16.0.1→16.1.6, motion→12.34.0
- Align lucide-react, sonner v2, tailwind-merge v3, postcss-selector-parser v7
- Remove framer-motion dep (superseded by motion)
- Fix @types/react-dom, TypeScript versions across workspaces
2026-03-03 07:34:51 -08:00

349 lines
10 KiB
TypeScript

"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import {
AnimatePresence,
motion,
PanInfo,
useMotionValue,
useTransform,
} from "motion/react"
import { cn } from "@/lib/utils"
import { Button } from "./button"
import { Card } from "./card"
export interface CarouselCard {
id: string
title: string
subtitle?: string
description?: string
image?: string
gradient?: string
content?: React.ReactNode
}
interface AppleCardsCarouselProps {
cards: CarouselCard[]
autoPlay?: boolean
autoPlayInterval?: number
className?: string
cardClassName?: string
showArrows?: boolean
showDots?: boolean
parallaxOffset?: number
stackOffset?: number
stackScale?: number
swipeThreshold?: number
}
export function AppleCardsCarousel({
cards,
autoPlay = false,
autoPlayInterval = 5000,
className,
cardClassName,
showArrows = true,
showDots = true,
parallaxOffset = 50,
stackOffset = 10,
stackScale = 0.95,
swipeThreshold = 50,
}: AppleCardsCarouselProps) {
const [currentIndex, setCurrentIndex] = React.useState(0)
const [isAutoPlaying, setIsAutoPlaying] = React.useState(autoPlay)
const containerRef = React.useRef<HTMLDivElement>(null)
const dragX = useMotionValue(0)
// Calculate card positions
const getCardStyle = (index: number) => {
const offset = index - currentIndex
const absOffset = Math.abs(offset)
const sign = Math.sign(offset)
// Only show 3 cards at a time for performance
if (absOffset > 2) {
return {
display: "none",
x: 0,
scale: 1,
opacity: 0,
zIndex: 0,
}
}
const baseX = offset * stackOffset
const scale = Math.pow(stackScale, absOffset)
const opacity = Math.max(0, 1 - absOffset * 0.3)
const zIndex = cards.length - absOffset
return {
display: "block",
x: baseX,
scale,
opacity,
zIndex,
}
}
// Navigate to next card
const goToNext = React.useCallback(() => {
setCurrentIndex((prev) => (prev + 1) % cards.length)
}, [cards.length])
// Navigate to previous card
const goToPrevious = React.useCallback(() => {
setCurrentIndex((prev) => (prev - 1 + cards.length) % cards.length)
}, [cards.length])
// Navigate to specific card
const goToCard = React.useCallback((index: number) => {
setCurrentIndex(index)
}, [])
// Handle drag end
const handleDragEnd = React.useCallback(
(event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
const { offset, velocity } = info
// Determine if we should navigate based on drag distance and velocity
if (Math.abs(offset.x) > swipeThreshold || Math.abs(velocity.x) > 500) {
if (offset.x > 0) {
goToPrevious()
} else {
goToNext()
}
}
},
[goToNext, goToPrevious, swipeThreshold]
)
// Auto-play functionality
React.useEffect(() => {
if (!isAutoPlaying) return
const interval = setInterval(goToNext, autoPlayInterval)
return () => clearInterval(interval)
}, [isAutoPlaying, goToNext, autoPlayInterval])
// Pause auto-play on hover
const handleMouseEnter = React.useCallback(() => {
if (autoPlay) {
setIsAutoPlaying(false)
}
}, [autoPlay])
const handleMouseLeave = React.useCallback(() => {
if (autoPlay) {
setIsAutoPlaying(true)
}
}, [autoPlay])
// Keyboard navigation
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
goToPrevious()
} else if (e.key === "ArrowRight") {
goToNext()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [goToNext, goToPrevious])
return (
<div
ref={containerRef}
className={cn("relative w-full max-w-6xl mx-auto px-4 py-8", className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Cards Container */}
<div className="relative h-[400px] md:h-[500px] lg:h-[600px] flex items-center justify-center">
<AnimatePresence mode="popLayout">
{cards.map((card, index) => {
const style = getCardStyle(index)
const isActive = index === currentIndex
return (
<motion.div
key={card.id}
className={cn(
"absolute inset-0 w-full h-full",
!isActive && "pointer-events-none"
)}
initial={false}
animate={{
x: style.x,
scale: style.scale,
opacity: style.opacity,
zIndex: style.zIndex,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
drag={isActive ? "x" : false}
dragSnapToOrigin
dragElastic={0.2}
onDragEnd={handleDragEnd}
whileDrag={{ cursor: "grabbing" }}
style={{
x: dragX,
display: style.display,
}}
>
<Card
className={cn(
"relative w-full h-full overflow-hidden cursor-grab",
"bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-950",
"shadow-2xl hover:shadow-3xl transition-shadow duration-300",
cardClassName
)}
style={{
background: card.gradient,
}}
>
{/* Parallax Background Image */}
{card.image && (
<motion.div
className="absolute inset-0 w-full h-full"
style={{
x: useTransform(
dragX,
[-200, 0, 200],
[parallaxOffset, 0, -parallaxOffset]
),
}}
>
<img
src={card.image}
alt={card.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
</motion.div>
)}
{/* Card Content */}
<div className="relative z-10 flex flex-col justify-end h-full p-8 md:p-12">
{card.content ? (
card.content
) : (
<>
<motion.h3
className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: isActive ? 1 : 0.7, y: 0 }}
transition={{ delay: 0.1 }}
>
{card.title}
</motion.h3>
{card.subtitle && (
<motion.p
className="text-xl md:text-2xl text-white/80 mb-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: isActive ? 1 : 0.7, y: 0 }}
transition={{ delay: 0.2 }}
>
{card.subtitle}
</motion.p>
)}
{card.description && (
<motion.p
className="text-base md:text-lg text-white/70 max-w-2xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: isActive ? 1 : 0.7, y: 0 }}
transition={{ delay: 0.3 }}
>
{card.description}
</motion.p>
)}
</>
)}
</div>
</Card>
</motion.div>
)
})}
</AnimatePresence>
</div>
{/* Navigation Arrows */}
{showArrows && (
<>
<Button
variant="ghost"
size="icon"
className={cn(
"absolute left-4 top-1/2 -translate-y-1/2 z-20",
"bg-white/10 backdrop-blur-md hover:bg-white/20",
"text-white border border-white/20",
"hidden md:flex"
)}
onClick={goToPrevious}
>
<ChevronLeft className="h-6 w-6" />
<span className="sr-only">Previous card</span>
</Button>
<Button
variant="ghost"
size="icon"
className={cn(
"absolute right-4 top-1/2 -translate-y-1/2 z-20",
"bg-white/10 backdrop-blur-md hover:bg-white/20",
"text-white border border-white/20",
"hidden md:flex"
)}
onClick={goToNext}
>
<ChevronRight className="h-6 w-6" />
<span className="sr-only">Next card</span>
</Button>
</>
)}
{/* Dot Indicators */}
{showDots && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 flex gap-2">
{cards.map((_, index) => (
<button
key={index}
className={cn(
"w-2 h-2 rounded-full transition-all duration-300",
"bg-white/40 hover:bg-white/60",
index === currentIndex && "w-8 bg-white"
)}
onClick={() => goToCard(index)}
>
<span className="sr-only">Go to card {index + 1}</span>
</button>
))}
</div>
)}
</div>
)
}
// Preset gradients for demo purposes
export const gradientPresets = {
sunset: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
ocean: "linear-gradient(135deg, #667eea 0%, #4ca1af 100%)",
fire: "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
forest: "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
galaxy: "linear-gradient(135deg, #7303c0 0%, #ec38bc 50%, #fdeff9 100%)",
aurora: "linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)",
peach: "linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)",
lavender: "linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)",
}