{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "casestudy-1",
  "title": "Case study 1: customer stories",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Customer stories for a marketing page: one featured story with its quote and results counting up, a grid of the others with a headline number each, an industry filter, and pictures drawn from each company's name.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/blur-fade.json",
    "https://ui.ballmac.com/r/cover-art.json",
    "https://ui.ballmac.com/r/number-ticker.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/casestudy-1/casestudy-1.tsx",
      "content": "// Ballmac UI: Case Study 1. https://ui.ballmac.com/blocks/casestudy-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Quote } from \"lucide-react\"\n\nimport { BlurFade } from \"@/components/ballmac/blur-fade\"\nimport { CoverArt, type CoverArtVariant } from \"@/components/ballmac/cover-art\"\nimport { NumberTicker } from \"@/components/ballmac/number-ticker\"\nimport { cn } from \"@/lib/utils\"\n\ntype CaseStudy1Metric = {\n  value: number\n  /** Text before the number, such as \"$\" or \"−\". */\n  prefix?: string\n  /** Text after the number, such as \"%\" or \"x\". */\n  suffix?: string\n  label: string\n}\n\ntype CaseStudy1Story = {\n  id: string\n  company: string\n  /** The group the story is filed under, used by the filter. */\n  industry: string\n  /** What happened, as a sentence. */\n  headline: string\n  summary?: string\n  quote?: string\n  person?: { name: string; role: string }\n  metrics: CaseStudy1Metric[]\n  href?: string\n}\n\ntype CaseStudy1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  eyebrow?: string\n  title?: string\n  description?: string\n  /** The first story is shown large; the rest sit in a grid below. */\n  stories?: CaseStudy1Story[]\n  /** Which kind of picture draws behind each story. */\n  art?: CoverArtVariant\n}\n\nconst sample: CaseStudy1Story[] = [\n  {\n    id: \"northwind\",\n    company: \"Northwind\",\n    industry: \"Logistics\",\n    headline: \"Northwind cut onboarding from three weeks to four days\",\n    summary: \"Seven regional teams ran on seven spreadsheets. One shared workspace replaced them and every new hire now starts with the same checklist.\",\n    quote: \"We stopped asking where the latest version lived. There is only one, and everybody can see it.\",\n    person: { name: \"Elena Marsh\", role: \"VP Operations\" },\n    metrics: [\n      { value: 81, suffix: \"%\", label: \"faster onboarding\" },\n      { value: 14, suffix: \"k\", label: \"hours saved a year\" },\n      { value: 3, suffix: \"x\", label: \"fewer handoff errors\" },\n    ],\n    href: \"#\",\n  },\n  {\n    id: \"lumen\",\n    company: \"Lumen Health\",\n    industry: \"Healthcare\",\n    headline: \"Lumen Health answers patients in minutes, not days\",\n    summary: \"Requests used to wait in a shared inbox. Now each one is routed, tracked and answered inside the day.\",\n    metrics: [{ value: 92, suffix: \"%\", label: \"answered the same day\" }, { value: 40, suffix: \"%\", label: \"fewer calls\" }],\n    href: \"#\",\n  },\n  {\n    id: \"orbit\",\n    company: \"Orbit Labs\",\n    industry: \"Software\",\n    headline: \"Orbit Labs shipped its redesign in six weeks, not six months\",\n    summary: \"A shared system of components let design and engineering stop arguing about buttons.\",\n    metrics: [{ value: 6, label: \"weeks to ship\" }, { value: 70, suffix: \"%\", label: \"less rework\" }],\n    href: \"#\",\n  },\n  {\n    id: \"fable\",\n    company: \"Fable & Co\",\n    industry: \"Retail\",\n    headline: \"Fable & Co grew repeat orders by a third in one quarter\",\n    summary: \"Clear order tracking and fewer surprises made customers come back.\",\n    metrics: [{ value: 34, suffix: \"%\", label: \"more repeat orders\" }, { value: 2, suffix: \"x\", label: \"reviews left\" }],\n    href: \"#\",\n  },\n]\n\nconst arts: CoverArtVariant[] = [\"mesh\", \"rings\", \"waves\", \"bars\", \"dots\"]\n\nfunction Mark({ name, className }: { name: string; className?: string }) {\n  return (\n    <span aria-hidden=\"true\" className={cn(\"bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-xl text-sm font-semibold\", className)}>\n      {name[0]}\n    </span>\n  )\n}\n\nfunction Metrics({ items, big }: { items: CaseStudy1Metric[]; big?: boolean }) {\n  return (\n    <dl className={cn(\"grid gap-x-8 gap-y-5\", big ? \"grid-cols-2 sm:grid-cols-3\" : \"grid-cols-2\")}>\n      {items.map((m) => (\n        <div key={m.label}>\n          <dd className={cn(\"font-semibold tracking-[-0.04em] tabular-nums\", big ? \"text-4xl sm:text-5xl\" : \"text-3xl\")}>\n            {m.prefix}\n            <NumberTicker value={m.value} />\n            {m.suffix}\n          </dd>\n          <dt className=\"text-muted-foreground mt-1 text-sm text-pretty\">{m.label}</dt>\n        </div>\n      ))}\n    </dl>\n  )\n}\n\n/**\n * Customer stories for a marketing page: the first story large, with its quote and results counting up, and the rest in a grid with one\n * headline number each. A filter by industry appears when there is more than one. Pictures are drawn from each company's name, so there are\n * no image files to supply and they stay the same on every load.\n */\nfunction CaseStudy1({\n  eyebrow = \"Customer stories\",\n  title = \"Teams that got their time back\",\n  description = \"What changed after they moved to a single place for the work, in their numbers and their words.\",\n  stories = sample,\n  art = \"mesh\",\n  className,\n  ...props\n}: CaseStudy1Props) {\n  const id = React.useId()\n  const industries = React.useMemo(() => [...new Set(stories.map((s) => s.industry))], [stories])\n  const [industry, setIndustry] = React.useState<string | null>(null)\n  const shown = industry ? stories.filter((s) => s.industry === industry) : stories\n  const [lead, ...rest] = shown\n\n  return (\n    <section data-slot=\"casestudy-1\" aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 sm:py-24\", className)} {...props}>\n      <header className=\"mx-auto max-w-2xl text-center\">\n        <p className=\"text-muted-foreground text-sm font-medium\">{eyebrow}</p>\n        <h2 id={`${id}-title`} className=\"mt-3 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl\">\n          {title}\n        </h2>\n        <p className=\"text-muted-foreground mt-4 text-lg text-pretty\">{description}</p>\n      </header>\n\n      {industries.length > 1 ? (\n        <div role=\"group\" aria-label=\"Filter by industry\" className=\"mt-10 flex flex-wrap justify-center gap-2\">\n          {[null, ...industries].map((i) => (\n            <button\n              key={i ?? \"all\"}\n              type=\"button\"\n              aria-pressed={industry === i}\n              onClick={() => setIndustry(i)}\n              className={cn(\"focus-visible:ring-ring/50 rounded-full border px-4 py-1.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", industry === i ? \"bg-foreground text-background border-foreground\" : \"hover:bg-accent\")}\n            >\n              {i ?? \"All\"}\n            </button>\n          ))}\n        </div>\n      ) : null}\n\n      {lead ? (\n        <BlurFade inView once>\n          <article className=\"bg-card mt-10 grid overflow-hidden rounded-3xl border lg:grid-cols-[minmax(0,5fr)_minmax(0,6fr)]\">\n            <div className=\"relative isolate min-h-64 overflow-hidden p-8 sm:p-10\">\n              <CoverArt seed={lead.company} variant={art} className=\"absolute inset-0 -z-10\" />\n              <div className=\"from-background/0 to-background/50 absolute inset-0 -z-10 bg-gradient-to-b\" aria-hidden=\"true\" />\n              <div className=\"flex items-center gap-3\">\n                <Mark name={lead.company} />\n                <p className=\"text-lg font-semibold tracking-tight\">{lead.company}</p>\n                <span className=\"bg-background/70 ms-auto rounded-full border px-3 py-1 text-xs font-medium backdrop-blur\">{lead.industry}</span>\n              </div>\n              {lead.quote ? (\n                <figure className=\"mt-16 sm:mt-24\">\n                  <Quote aria-hidden=\"true\" className=\"size-7 opacity-60 rtl:-scale-x-100\" />\n                  <blockquote className=\"mt-3 text-xl leading-snug font-medium tracking-tight text-pretty sm:text-2xl\">{lead.quote}</blockquote>\n                  {lead.person ? (\n                    <figcaption className=\"mt-5 text-sm\">\n                      <span className=\"font-semibold\">{lead.person.name}</span>\n                      <span className=\"text-foreground/70\">, {lead.person.role}</span>\n                    </figcaption>\n                  ) : null}\n                </figure>\n              ) : null}\n            </div>\n            <div className=\"flex flex-col justify-between gap-10 p-8 sm:p-10\">\n              <div>\n                <h3 className=\"text-2xl font-semibold tracking-[-0.03em] text-balance sm:text-3xl\">{lead.headline}</h3>\n                {lead.summary ? <p className=\"text-muted-foreground mt-4 text-base text-pretty\">{lead.summary}</p> : null}\n              </div>\n              <Metrics items={lead.metrics} big />\n              {lead.href ? (\n                <a href={lead.href} className=\"group focus-visible:ring-ring/50 inline-flex w-fit items-center gap-2 rounded-md text-sm font-semibold outline-none focus-visible:ring-[3px]\">\n                  Read the story <span className=\"sr-only\">about {lead.company}</span>\n                  <ArrowRight aria-hidden=\"true\" className=\"size-4 transition-transform group-hover:translate-x-1 rtl:rotate-180 rtl:group-hover:-translate-x-1 motion-reduce:transition-none\" />\n                </a>\n              ) : null}\n            </div>\n          </article>\n        </BlurFade>\n      ) : null}\n\n      {rest.length ? (\n        <ul className=\"mt-6 grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n          {rest.map((s, i) => (\n            <li key={s.id}>\n              <BlurFade inView once delay={i * 0.06} className=\"h-full\">\n                <article className=\"bg-card group relative flex h-full flex-col overflow-hidden rounded-3xl border transition-shadow duration-200 hover:shadow-lg motion-reduce:transition-none\">\n                  <div className=\"relative isolate flex h-36 items-end overflow-hidden p-5\">\n                    <CoverArt seed={s.company} variant={arts[(i + 1) % arts.length]} className=\"absolute inset-0 -z-10 transition-transform duration-500 group-hover:scale-105 motion-reduce:transition-none\" />\n                    <div aria-hidden=\"true\" className=\"from-background/80 absolute inset-x-0 bottom-0 -z-10 h-20 bg-gradient-to-t to-transparent\" />\n                    <div className=\"flex w-full items-center gap-3\">\n                      <Mark name={s.company} className=\"size-8 text-xs\" />\n                      <p className=\"font-semibold tracking-tight\">{s.company}</p>\n                      <span className=\"bg-background/70 ms-auto rounded-full border px-2.5 py-0.5 text-xs font-medium backdrop-blur\">{s.industry}</span>\n                    </div>\n                  </div>\n                  <div className=\"flex flex-1 flex-col gap-5 p-6\">\n                    <h3 className=\"text-lg leading-snug font-semibold tracking-tight text-balance\">\n                      {s.href ? (\n                        <a href={s.href} className=\"focus-visible:ring-ring/50 rounded-sm outline-none after:absolute after:inset-0 focus-visible:ring-[3px]\">\n                          {s.headline}\n                        </a>\n                      ) : (\n                        s.headline\n                      )}\n                    </h3>\n                    {s.summary ? <p className=\"text-muted-foreground text-sm text-pretty\">{s.summary}</p> : null}\n                    <div className=\"mt-auto border-t pt-5\">\n                      <Metrics items={s.metrics.slice(0, 2)} />\n                    </div>\n                  </div>\n                </article>\n              </BlurFade>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </section>\n  )\n}\n\nexport { CaseStudy1, type CaseStudy1Props, type CaseStudy1Story, type CaseStudy1Metric }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/casestudy-1/casestudy-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "case study",
      "customer stories",
      "social proof",
      "results",
      "metrics",
      "testimonials",
      "marketing",
      "success stories"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "Customer stories. <CaseStudy1 stories={[{ id, company, industry, headline, summary, quote, person: { name, role }, metrics: [{ value: 81, suffix: '%', label }], href }]} art='mesh' | 'rings' | 'waves' | 'bars' | 'dots' />. The first story is featured large with its quote; the rest form a grid. An industry filter shows when there are two or more industries. Numbers count up once on scroll and show their final value with reduced motion.",
      "whenToUse": [
        "The social proof section of a marketing site",
        "A customers page linking to full stories"
      ],
      "whenNotToUse": [
        "Short quotes only (use testimonials-1 or testimonials-2)",
        "A row of customer logos (use logo-cloud-1)"
      ],
      "composesWith": [
        "hero-1",
        "testimonials-1",
        "cta-1",
        "logo-cloud-1",
        "footer-1"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Industry buttons (pressed state announced), then one link per story; the whole card is clickable through its headline link"
        },
        {
          "keys": "Results",
          "action": "Each result is a term and its number in a description list, so a screen reader hears the label with the value"
        },
        {
          "keys": "Artwork",
          "action": "Decorative and hidden from assistive technology"
        }
      ],
      "customization": [
        "art picks the picture style of the featured story; the cards vary on their own",
        "Pictures come from the company name, so identical on server and client",
        "Arrows and the card layout mirror in right-to-left"
      ]
    },
    "statesNone": "Static content: nothing to load, submit or fail.",
    "examples": [
      "casestudy-1-demo",
      "casestudy-1-rings"
    ],
    "url": "https://ui.ballmac.com/components/casestudy-1"
  },
  "categories": [
    "blocks",
    "casestudy"
  ],
  "type": "registry:block"
}