{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "preview-switch-hero", "type": "registry:ui", "title": "Preview Switch Hero", "description": "A split hero with an icon-rail that switches the product preview, a lead-capture column (badge, ratings, email form, CTAs, avatars), and a full-width logo strip. Responsive and theme-aware.", "dependencies": [ "lucide-react" ], "files": [ { "path": "registry/ruixenui/preview-switch-hero.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { ArrowUpRight, Mail, Star } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport interface PreviewTab {\n id: string;\n /** Text label shown in the switcher rail. */\n label: string;\n /** Panel shown when this tab is active. All panels should share one size. */\n media: React.ReactNode;\n}\n\nexport interface HeroRating {\n source: string;\n score: string;\n /** Defaults to a filled star. */\n icon?: React.ReactNode;\n}\n\nexport interface HeroLogo {\n name: string;\n /** Optional custom mark; falls back to the name as a text wordmark. */\n logo?: React.ReactNode;\n}\n\nexport interface HeroAvatar {\n initials?: string;\n src?: string;\n}\n\nexport interface Cta {\n label: string;\n href?: string;\n}\n\nexport interface PreviewSwitchHeroProps {\n /** Small pill above the title, e.g. `{ tag: \"NEW\", label: \"…\" }`. */\n badge?: { tag?: string; label: React.ReactNode };\n title: React.ReactNode;\n description?: React.ReactNode;\n ratings?: HeroRating[];\n /**\n * Show the email-capture field above the CTAs. When `false`, the CTAs render\n * on their own as plain actions (no form/input). Default `true`.\n */\n showEmail?: boolean;\n /** Label above the email field. */\n emailLabel?: React.ReactNode;\n emailPlaceholder?: string;\n /** Called with the email on submit. */\n onSubmit?: (email: string) => void;\n /** Submits the form when no `href`; otherwise renders as a link. */\n primaryCta?: Cta;\n secondaryCta?: Cta;\n avatars?: HeroAvatar[];\n socialProof?: React.ReactNode;\n /** Text tabs that switch the preview. */\n tabs: PreviewTab[];\n /** Logo strip rendered full-width below the split. */\n logos?: HeroLogo[];\n className?: string;\n}\n\n/* ── pieces ──────────────────────────────────────────────────── */\n\nfunction TabRail({\n tabs,\n active,\n onSelect,\n}: {\n tabs: PreviewTab[];\n active: number;\n onSelect: (i: number) => void;\n}) {\n return (\n \n {tabs.map((t, i) => {\n const isActive = i === active;\n return (\n onSelect(i)}\n className={cn(\n \"whitespace-nowrap rounded-xl px-4 py-2.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n isActive\n ? \"bg-muted font-semibold text-foreground shadow-sm ring-1 ring-border\"\n : \"font-medium text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n )}\n >\n {t.label}\n \n );\n })}\n \n );\n}\n\nfunction PreviewStack({\n tabs,\n active,\n}: {\n tabs: PreviewTab[];\n active: number;\n}) {\n return (\n
\n {tabs.map((t, i) => {\n const isActive = i === active;\n return (\n \n {t.media}\n
\n );\n })}\n \n );\n}\n\nfunction CtaButton({\n cta,\n variant,\n type,\n}: {\n cta: Cta;\n variant: \"primary\" | \"secondary\";\n type?: \"submit\" | \"button\";\n}) {\n const className = cn(\n \"inline-flex h-10 items-center justify-center gap-2 whitespace-nowrap rounded-xl px-3.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n variant === \"primary\"\n ? \"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90\"\n : \"bg-muted text-muted-foreground hover:bg-background hover:text-foreground hover:shadow-sm hover:ring-1 hover:ring-border\",\n );\n const body = (\n <>\n {cta.label}\n {variant === \"primary\" && }\n \n );\n if (cta.href) {\n return (\n \n {body}\n \n );\n }\n return (\n \n );\n}\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function PreviewSwitchHero({\n badge,\n title,\n description,\n ratings,\n showEmail = true,\n emailLabel = \"Enter email address\",\n emailPlaceholder = \"you@example.com\",\n onSubmit,\n primaryCta,\n secondaryCta,\n avatars,\n socialProof,\n tabs,\n logos,\n className,\n}: PreviewSwitchHeroProps) {\n const emailId = React.useId();\n const [active, setActive] = React.useState(0);\n\n // Tabs are click-driven; clicking one swaps the preview in place.\n const handleSelect = (i: number) => setActive(i);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const data = new FormData(e.currentTarget);\n onSubmit?.(String(data.get(\"email\") ?? \"\"));\n };\n\n return (\n \n
\n
\n
\n {/* ── left: text-tab rail + switchable preview ─────── */}\n \n \n \n
\n\n {/* ── right: content (centered on mobile, left on md+) ── */}\n
\n {badge && (\n
\n {badge.tag && (\n \n {badge.tag}\n \n )}\n \n {badge.label}\n \n
\n )}\n\n

\n {title}\n

\n\n {description && (\n

\n {description}\n

\n )}\n\n {ratings && ratings.length > 0 && (\n
\n {ratings.map((r, i) => (\n \n {r.icon ?? (\n \n )}\n \n {r.score}\n \n \n {r.source}\n \n
\n ))}\n
\n )}\n\n {showEmail ? (\n
\n
\n \n {emailLabel}\n \n
\n \n \n
\n\n {(primaryCta || secondaryCta) && (\n
\n {primaryCta && (\n \n )}\n {secondaryCta && (\n \n )}\n
\n )}\n
\n
\n ) : (\n (primaryCta || secondaryCta) && (\n
\n {primaryCta && (\n \n )}\n {secondaryCta && (\n \n )}\n
\n )\n )}\n\n {(avatars?.length || socialProof) && (\n
\n {avatars && avatars.length > 0 && (\n
\n {avatars.map((a, i) => (\n 0 && \"-ml-2\",\n )}\n >\n {a.src ? (\n // eslint-disable-next-line @next/next/no-img-element\n \n ) : (\n a.initials\n )}\n \n ))}\n
\n )}\n {socialProof && (\n \n {socialProof}\n \n )}\n
\n )}\n
\n
\n \n\n {/* ── logo strip ─────────────────────────────────────── */}\n {logos && logos.length > 0 && (\n
\n
\n
\n {logos.map((l, i) => (\n \n
\n {l.logo ?? (\n \n {l.name}\n \n )}\n
\n {i < logos.length - 1 && (\n \n )}\n
\n ))}\n
\n
\n \n )}\n \n \n );\n}\n\nexport default PreviewSwitchHero;\n", "type": "registry:ui", "target": "components/ruixen/preview-switch-hero.tsx" } ] }