FAQ

FAQ 02 — Two Columns

Static two-column FAQ grid with numbered markers, staggered reveals and a glowing contact CTA row.

$ npx shadcn@latest add @ludusvibe/faq-02
Open in v0

Code

Installed to components/sections/faq-02.tsx — it's yours to edit.

"use client"

import * as React from "react"
import { motion } from "motion/react"
import { ArrowRight } from "lucide-react"

import { buttonVariants } from "@/components/ui/button"
import { Reveal } from "@/components/ludus/reveal"
import { fadeUp, stagger, viewport } from "@/lib/motion"
import { cn } from "@/lib/utils"

export type Faq02Item = {
  question: string
  answer: React.ReactNode
}

export type Faq02Props = {
  eyebrow?: string
  title?: React.ReactNode
  description?: string
  items?: Faq02Item[]
  contactTitle?: string
  contactDescription?: string
  contactCta?: { label: string; href: string }
  /** Heading element — use "h1" when this is the page's main heading. */
  as?: "h1" | "h2"
  className?: string
}

const defaultItems: Faq02Item[] = [
  {
    question: "How does the 14-day free trial work?",
    answer:
      "Every plan starts with a full-featured 14-day trial — no credit card required. If you don't upgrade, your account simply drops to read-only until you do.",
  },
  {
    question: "Can I change plans later?",
    answer:
      "Yes. Upgrade, downgrade or switch billing intervals at any time. Upgrades take effect immediately and are prorated; downgrades apply next cycle.",
  },
  {
    question: "What counts as a monthly page view?",
    answer:
      "A single load of any page you publish with Lumen. Bot and crawler traffic is filtered out automatically and never counts against your quota.",
  },
  {
    question: "How do you handle data privacy?",
    answer:
      "All data is encrypted in transit and at rest, and we're GDPR and CCPA compliant. On Pro and above you can choose EU-only data residency.",
  },
  {
    question: "Can I cancel anytime?",
    answer:
      "Absolutely — two clicks from your billing settings. Your plan stays active until the end of the paid period, and you can export all of your data.",
  },
  {
    question: "What kind of support is included?",
    answer:
      "Community support on Starter, priority email with a 4-hour first-response target on Pro, and a dedicated success manager on Enterprise.",
  },
]

export function Faq02({
  eyebrow = "FAQ",
  title = "Answers, before you even ask",
  description = "The short version of everything teams ask us about Lumen, billing and data.",
  items = defaultItems,
  contactTitle = "Didn't find your answer?",
  contactDescription = "Our team replies to every message — usually within a couple of hours.",
  contactCta = { label: "Contact support", href: "#" },
  as: Heading = "h2",
  className,
}: Faq02Props) {
  return (
    <section className={cn("bg-background px-6 py-24 md:py-32", className)}>
      <div className="mx-auto max-w-5xl">
        <div className="mx-auto max-w-2xl text-center">
          <Reveal>
            <p className="text-sm font-medium tracking-widest text-primary uppercase">
              {eyebrow}
            </p>
          </Reveal>
          <Reveal delay={0.1}>
            <Heading className="mt-4 text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
              {title}
            </Heading>
          </Reveal>
          <Reveal delay={0.2}>
            <p className="mt-5 text-pretty text-muted-foreground md:text-lg">
              {description}
            </p>
          </Reveal>
        </div>

        <motion.div
          initial="hidden"
          whileInView="visible"
          viewport={viewport}
          variants={stagger()}
          className="mt-14 grid gap-x-12 gap-y-10 md:mt-16 md:grid-cols-2"
        >
          {items.map((item, index) => (
            <motion.div key={index} variants={fadeUp} className="flex gap-4">
              <span
                aria-hidden
                className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 font-heading text-sm font-semibold text-primary"
              >
                {String(index + 1).padStart(2, "0")}
              </span>
              <div>
                <h3 className="font-medium">{item.question}</h3>
                <p className="mt-2 text-sm text-pretty text-muted-foreground">
                  {item.answer}
                </p>
              </div>
            </motion.div>
          ))}
        </motion.div>

        <Reveal delay={0.2} className="mt-14 md:mt-16">
          <div className="relative overflow-hidden rounded-2xl border bg-card p-6 sm:px-8">
            <div
              aria-hidden
              className="absolute -top-16 -right-16 size-40 rounded-full bg-primary/15 blur-3xl"
            />
            <div className="relative flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
              <div>
                <p className="font-heading font-semibold tracking-tight">
                  {contactTitle}
                </p>
                <p className="mt-1 text-sm text-muted-foreground">
                  {contactDescription}
                </p>
              </div>
              <a
                href={contactCta.href}
                className={cn(buttonVariants({ size: "lg" }), "group shrink-0")}
              >
                {contactCta.label}
                <ArrowRight className="transition-transform group-hover:translate-x-0.5" />
              </a>
            </div>
          </div>
        </Reveal>
      </div>
    </section>
  )
}