{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "pricing-landing-hero", "type": "registry:ui", "title": "Pricing Landing Hero", "description": "A service-style landing hero with scattered preview cards, price block with strike-through, scarcity line, dual CTAs, and trusted-by strip.", "files": [ { "path": "registry/ruixenui/pricing-landing-hero.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { Battery, Signal, Wifi } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\ntype Action = { href: string; label: string };\n\ninterface PriceBlock {\n /** Current price, e.g. \"$850\" */\n current: string;\n /** Original price shown with strike-through, e.g. \"$2,000\" */\n original?: string;\n}\n\ninterface TrustedBySection {\n heading?: string;\n logos: React.ReactNode[];\n}\n\ninterface PhoneSheetItem {\n title: string;\n description: string;\n}\n\ninterface PhoneMockupProps {\n /** Time shown in the status bar. */\n time?: string;\n /**\n * Items to cycle through inside the sheet. When more than one is provided\n * they crossfade automatically every `cycleMs` milliseconds.\n */\n items?: PhoneSheetItem[];\n /** Interval between item transitions (ms). */\n cycleMs?: number;\n}\n\nexport interface PricingLandingHeroProps {\n /** Main headline */\n title: React.ReactNode;\n /** Subtitle / description text */\n description?: string;\n /** Phone mockup shown above the headline */\n phone?: PhoneMockupProps;\n /** Price block shown between description and CTAs */\n price?: PriceBlock;\n /** Small availability / scarcity line above the CTAs */\n availability?: string;\n /** Primary CTA button (filled) */\n primaryAction?: Action;\n /** Secondary CTA button (outlined) */\n secondaryAction?: Action;\n /** Trusted-by logo strip */\n trustedBy?: TrustedBySection;\n className?: string;\n}\n\n/* ── phone mockup ────────────────────────────────────────────── */\n\nconst DEFAULT_ITEMS: PhoneSheetItem[] = [\n { title: \"Add a Title...\", description: \"Add a description...\" },\n];\n\nconst SPRING = \"cubic-bezier(0.22, 1, 0.36, 1)\";\n\nfunction SheetCardContent({ item }: { item: PhoneSheetItem }) {\n return (\n <>\n {/* Drag handle bar — part of each card, slides with it */}\n
\n
\n {item.title}\n
\n
\n {item.description}\n
\n \n );\n}\n\n/**\n * A sheet card that mounts at translateY(100%) and springs to 0 — used as\n * the \"top\" layer that slides over the previous card. Remounted via `key`\n * each tick so the animation replays.\n */\nfunction IncomingSheetCard({ item }: { item: PhoneSheetItem }) {\n const [raised, setRaised] = React.useState(false);\n\n React.useEffect(() => {\n // Double rAF ensures the initial translateY(100%) paint lands before\n // we flip to translateY(0) — otherwise the browser skips the transition.\n let cancelled = false;\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n if (cancelled) return;\n raf2 = requestAnimationFrame(() => {\n if (!cancelled) setRaised(true);\n });\n });\n return () => {\n cancelled = true;\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }, []);\n\n return (\n \n \n
\n );\n}\n\nfunction AnimatedPhoneMockup({\n time = \"9:41\",\n items = DEFAULT_ITEMS,\n cycleMs = 2800,\n}: PhoneMockupProps) {\n const [idx, setIdx] = React.useState(0);\n const [mounted, setMounted] = React.useState(false);\n\n // Sheet rise on mount\n React.useEffect(() => {\n const t = setTimeout(() => setMounted(true), 120);\n return () => clearTimeout(t);\n }, []);\n\n // Cycle items\n React.useEffect(() => {\n if (items.length <= 1) return;\n const id = setInterval(\n () => setIdx((i) => (i + 1) % items.length),\n cycleMs,\n );\n return () => clearInterval(id);\n }, [items.length, cycleMs]);\n\n const prevIdx = idx === 0 ? items.length - 1 : idx - 1;\n\n return (\n \n {/* Outer bezel */}\n
\n {/* Screen */}\n
\n {/* Status bar */}\n
\n {time}\n
\n \n \n \n
\n
\n\n {/* Sheet backdrop */}\n
\n {/* Rising sheet */}\n \n {/* Stacked sheet cards — new card slides up over previous,\n drag-handle bar slides as part of each card */}\n
\n {/* Backdrop: the previous card, static at rest */}\n \n \n
\n {/* Incoming: current card, slides up from below on each tick */}\n \n
\n
\n
\n \n \n \n );\n}\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function PricingLandingHero({\n title,\n description,\n phone,\n price,\n availability,\n primaryAction,\n secondaryAction,\n trustedBy,\n className,\n}: PricingLandingHeroProps) {\n return (\n \n
\n {/* ── phone mockup ─────────────────────────────────── */}\n
\n \n
\n\n {/* ── heading + description ────────────────────────── */}\n

\n {title}\n

\n\n {description && (\n

\n {description}\n

\n )}\n\n {/* ── price block ──────────────────────────────────── */}\n {price && (\n
\n \n {price.current}\n \n {price.original && (\n \n {price.original}\n \n )}\n
\n )}\n\n {/* ── availability ─────────────────────────────────── */}\n {availability && (\n

{availability}

\n )}\n\n {/* ── CTAs ─────────────────────────────────────────── */}\n {(primaryAction || secondaryAction) && (\n
\n {primaryAction && (\n \n {primaryAction.label}\n \n )}\n {secondaryAction && (\n \n {secondaryAction.label}\n \n )}\n
\n )}\n\n {/* ── trusted-by strip ─────────────────────────────── */}\n {trustedBy && trustedBy.logos.length > 0 && (\n
\n

\n {trustedBy.heading || \"Trusted by\"}\n

\n
\n {trustedBy.logos.map((logo, idx) => (\n
\n {logo}\n
\n ))}\n
\n
\n )}\n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/pricing-landing-hero.tsx" } ] }