{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "faq-2",
  "title": "FAQ 2: searchable with topic filters",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Frequently asked questions with live search that highlights matches, topic filters with counts, a friendly empty state and a support prompt. Topics scroll sideways on phones.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/accordion.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/search-field.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/faq-2/faq-2.tsx",
      "content": "// Ballmac UI: FAQ 2. https://ui.ballmac.com/blocks/faq-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { LifeBuoy, SearchX } from \"lucide-react\"\n\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@/components/ballmac/accordion\"\nimport { buttonVariants } from \"@/components/ballmac/button\"\nimport { SearchField } from \"@/components/ballmac/search-field\"\nimport { cn } from \"@/lib/utils\"\n\ntype Faq2Question = {\n  /** The question. */\n  question: string\n  /** The answer, as plain text. */\n  answer: string\n}\n\ntype Faq2Category = {\n  /** Category name. */\n  name: string\n  /** Questions in this category. */\n  questions: Faq2Question[]\n}\n\ntype Faq2Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Section heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** Question groups. An \"All\" filter is added in front. */\n  categories?: Faq2Category[]\n  /** Contact shown when nothing matches, and under the list. */\n  support?: { label: string; href: string; text?: string } | null\n  /** Placeholder for the search field. */\n  searchPlaceholder?: string\n}\n\nconst defaults: Faq2Category[] = [\n  {\n    name: \"Billing\",\n    questions: [\n      { question: \"Can I change plans at any time?\", answer: \"Yes. Upgrades take effect immediately and we prorate the difference. Downgrades start at the end of your billing period.\" },\n      { question: \"Do you offer refunds?\", answer: \"If you are not happy within the first 30 days of a paid plan, email us and we will refund you in full, no questions asked.\" },\n      { question: \"Which payment methods do you accept?\", answer: \"All major credit and debit cards, plus invoices and bank transfer on annual Scale plans.\" },\n      { question: \"Is VAT included in the price?\", answer: \"Prices exclude taxes. VAT or sales tax is calculated at checkout based on your billing address.\" },\n    ],\n  },\n  {\n    name: \"Security\",\n    questions: [\n      { question: \"Is my data encrypted?\", answer: \"Everything is encrypted in transit with TLS 1.3 and at rest with AES-256. Backups use separate keys.\" },\n      { question: \"Do you support single sign-on?\", answer: \"SAML SSO and SCIM provisioning are available on Pro and Scale plans and work with Okta, Entra ID and Google Workspace.\" },\n      { question: \"Where is my data stored?\", answer: \"You choose a region when you create a workspace: United States, European Union or United Kingdom. Data stays in that region.\" },\n    ],\n  },\n  {\n    name: \"Product\",\n    questions: [\n      { question: \"How do I import data from another tool?\", answer: \"Use the import wizard in Settings to upload a CSV or connect your old account. Most teams finish in under an hour.\" },\n      { question: \"Is there an API?\", answer: \"Yes. Every action in the app is available through a REST API and webhooks, with official SDKs for TypeScript, Python and Go.\" },\n      { question: \"Can I invite people outside my company?\", answer: \"Guests can view and comment on shared items without taking a paid seat. Pro includes 50 guests and Scale is unlimited.\" },\n    ],\n  },\n]\n\nfunction highlight(text: string, query: string) {\n  const q = query.trim()\n  if (!q) return text\n  const parts = text.split(new RegExp(`(${q.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")})`, \"gi\"))\n  return parts.map((part, i) =>\n    part.toLowerCase() === q.toLowerCase() ? (\n      <mark key={i} className=\"bg-chart-3/30 text-foreground rounded-sm px-0.5\">\n        {part}\n      </mark>\n    ) : (\n      part\n    )\n  )\n}\n\nfunction Faq2({\n  title = \"Questions, answered.\",\n  description = \"Search the list or pick a topic. Still stuck? We reply within a day.\",\n  categories = defaults,\n  support = { label: \"Contact support\", href: \"#\", text: \"Can’t find what you need?\" },\n  searchPlaceholder = \"Search questions\",\n  className,\n  ...props\n}: Faq2Props) {\n  const [category, setCategory] = React.useState(\"All\")\n  const [query, setQuery] = React.useState(\"\")\n  const q = query.trim().toLowerCase()\n\n  const visible = categories\n    .filter((c) => category === \"All\" || c.name === category)\n    .flatMap((c) => c.questions.map((item) => ({ ...item, category: c.name })))\n    .filter((item) => !q || item.question.toLowerCase().includes(q) || item.answer.toLowerCase().includes(q))\n  const names = [\"All\", ...categories.map((c) => c.name)]\n  const total = categories.reduce((n, c) => n + c.questions.length, 0)\n\n  return (\n    <section data-slot=\"faq-2\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"grid gap-10 lg:grid-cols-[20rem_1fr] lg:gap-16\">\n        <div className=\"min-w-0\">\n          <h2 className=\"text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl\">{title}</h2>\n          <p className=\"text-muted-foreground mt-4 text-pretty\">{description}</p>\n          <SearchField className=\"mt-6\" label=\"Search questions\" placeholder={searchPlaceholder} value={query} onValueChange={setQuery} />\n          <div role=\"group\" aria-label=\"Topics\" className=\"mt-5 flex gap-1.5 overflow-x-auto pb-1 [scrollbar-width:none] lg:flex-col lg:overflow-visible\">\n            {names.map((name) => {\n              const on = category === name\n              const n = name === \"All\" ? total : (categories.find((c) => c.name === name)?.questions.length ?? 0)\n              return (\n                <button\n                  key={name}\n                  type=\"button\"\n                  aria-pressed={on}\n                  onClick={() => setCategory(name)}\n                  className={cn(\n                    \"focus-visible:ring-ring/50 flex h-9 shrink-0 items-center justify-between gap-3 rounded-full border px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] lg:rounded-xl\",\n                    on ? \"bg-foreground text-background border-transparent\" : \"text-muted-foreground hover:text-foreground hover:bg-accent\"\n                  )}\n                >\n                  {name}\n                  <span className={cn(\"text-xs tabular-nums\", on ? \"text-background/70\" : \"text-muted-foreground\")}>{n}</span>\n                </button>\n              )\n            })}\n          </div>\n        </div>\n\n        <div className=\"min-w-0\">\n          <p role=\"status\" className=\"sr-only\">\n            {visible.length === 0 ? \"No questions match your search.\" : `${visible.length} ${visible.length === 1 ? \"question\" : \"questions\"} shown.`}\n          </p>\n          {visible.length > 0 ? (\n            <Accordion type=\"single\" collapsible className=\"divide-y rounded-2xl border px-5 sm:px-6\">\n              {visible.map((item) => (\n                <AccordionItem key={item.question} value={item.question} className=\"border-b-0\">\n                  <AccordionTrigger className=\"py-5 text-base\">\n                    <span className=\"flex flex-col items-start gap-1 text-start\">\n                      {category === \"All\" && <span className=\"text-muted-foreground text-xs font-normal\">{item.category}</span>}\n                      {highlight(item.question, query)}\n                    </span>\n                  </AccordionTrigger>\n                  <AccordionContent className=\"text-muted-foreground text-base leading-relaxed text-pretty\">{highlight(item.answer, query)}</AccordionContent>\n                </AccordionItem>\n              ))}\n            </Accordion>\n          ) : (\n            <div className=\"flex flex-col items-center rounded-2xl border border-dashed px-6 py-14 text-center\">\n              <span className=\"bg-muted flex size-12 items-center justify-center rounded-full\"><SearchX className=\"size-5\" aria-hidden=\"true\" /></span>\n              <p className=\"mt-4 font-semibold\">No answers found</p>\n              <p className=\"text-muted-foreground mt-1 max-w-sm text-sm\">Nothing matches “{query}”. Try a shorter word, or ask us directly.</p>\n              <button type=\"button\" onClick={() => { setQuery(\"\"); setCategory(\"All\") }} className={buttonVariants({ variant: \"outline\", size: \"sm\", shape: \"pill\", className: \"mt-5\" })}>\n                Reset filters\n              </button>\n            </div>\n          )}\n          {support && (\n            <div className=\"bg-muted/40 mt-6 flex flex-col items-start gap-3 rounded-2xl border p-5 sm:flex-row sm:items-center sm:justify-between\">\n              <p className=\"flex items-center gap-2.5 text-sm\">\n                <LifeBuoy className=\"text-muted-foreground size-4\" aria-hidden=\"true\" />\n                {support.text ?? \"Still have questions?\"}\n              </p>\n              <a href={support.href} className={buttonVariants({ variant: \"outline\", size: \"sm\", shape: \"pill\" })}>{support.label}</a>\n            </div>\n          )}\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Faq2, type Faq2Props, type Faq2Category, type Faq2Question }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/faq-2/faq-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "faq",
      "search",
      "accordion",
      "help",
      "support",
      "filter"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A help-centre style FAQ. Pass categories=[{ name, questions: [{ question, answer }] }]; an All filter and search are built in.",
      "whenToUse": [
        "More than eight questions across topics",
        "Pricing and product pages that attract many questions"
      ],
      "whenNotToUse": [
        "A handful of questions (use faq-1)"
      ],
      "composesWith": [
        "pricing-2",
        "pricing-3",
        "cta-1",
        "footer-1"
      ],
      "a11y": [
        {
          "keys": "Enter / Space on a topic",
          "action": "Filters the list (aria-pressed)"
        },
        {
          "keys": "Typing in search",
          "action": "Filters live; the result count is announced politely"
        }
      ],
      "customization": [
        "categories: { name, questions: { question, answer }[] }[]",
        "support: { label, href, text? } (null to hide)"
      ]
    },
    "examples": [
      "faq-2-demo",
      "faq-2-compact"
    ],
    "url": "https://ui.ballmac.com/components/faq-2"
  },
  "categories": [
    "blocks",
    "faq"
  ],
  "type": "registry:block"
}