{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "customer-story-stack", "type": "registry:ui", "title": "Customer Story Stack", "description": "A card-stack customer testimonial carousel with vertical prev/next nav, touch-swipe support, two ghost cards layered behind the active story, and a framed metrics panel with corner crosshair marks that updates per case.", "dependencies": [ "lucide-react" ], "files": [ { "path": "registry/ruixenui/customer-story-stack.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport interface CustomerStoryAuthor {\n name: string;\n role: string;\n /** Image URL for the avatar. */\n avatarUrl?: string;\n /** Initials/letter fallback when `avatarUrl` is omitted. */\n avatarFallback?: string;\n}\n\nexport interface CustomerStoryMetric {\n /** Single icon rendered above the metric label. */\n icon: React.ReactNode;\n /** Label rendered below the icon. */\n label: React.ReactNode;\n}\n\nexport interface CustomerStoryCase {\n /** Unique id used as the React key when cycling cards. */\n id: string;\n /** Brand logo — typically an inline SVG. */\n logo: React.ReactNode;\n /** Pull-quote rendered with serif curly-quote pseudo-elements. */\n quote: React.ReactNode;\n author: CustomerStoryAuthor;\n /** Two metrics shown inside the same frame, below the testimonial. Omit to hide. */\n metrics?: [CustomerStoryMetric, CustomerStoryMetric];\n}\n\nexport interface CustomerStoryStackProps {\n cases: CustomerStoryCase[];\n /** Optional \"Read more customer stories\" link rendered below the frame. */\n readMoreLink?: { label: string; href: string };\n /** Horizontal swipe threshold in pixels for touch-based prev/next. */\n swipeThreshold?: number;\n className?: string;\n}\n\n/* ── primitives ──────────────────────────────────────────────── */\n\nfunction Crosshair({ className }: { className?: string }) {\n return (\n
\n );\n}\n\nfunction FrameLines() {\n const lineH =\n \"pointer-events-none absolute left-0 h-px w-full bg-[linear-gradient(to_right,_transparent_0%,_var(--color-border)_6.5%,_var(--color-border)_93.5%,_transparent_100%)]\";\n const lineV =\n \"pointer-events-none absolute top-0 h-full w-px bg-[linear-gradient(to_bottom,_transparent_0%,_var(--color-border)_6.5%,_var(--color-border)_93.5%,_transparent_100%)]\";\n return (\n <>\n \n \n \n \n\n {activeCase.quote}\n
\n\n\n {activeCase.author.name}\n
\n\n {activeCase.author.role}\n
\n\n {metric.label}\n
\n