{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "case-study-tabs",
  "type": "registry:ui",
  "title": "Case Study Tabs",
  "description": "A tabbed customer-testimonial block with embossed stars, brand-logo tabs, an animated underline indicator that adopts each brand's accent color, a featured case-study paragraph with CTA, and a pull-quote with author.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "tabs"
  ],
  "files": [
    {
      "path": "registry/ruixenui/case-study-tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { ChevronRight, Star } from \"lucide-react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport interface CaseStudyTabsBrand {\n  /** Unique tab id. */\n  id: string;\n  /** Accessible label for screen readers. */\n  name: string;\n  /** Logo node — typically an inline SVG. */\n  logo: React.ReactNode;\n  /** CSS color string used for the animated underline indicator. Defaults to `currentColor`. */\n  accentColor?: string;\n}\n\nexport interface CaseStudyAuthor {\n  name: string;\n  role: string;\n  /** Image URL for the avatar. */\n  avatarUrl?: string;\n  /** Initials/letter fallback when `avatarUrl` is omitted. */\n  avatarFallback?: string;\n}\n\nexport interface CaseStudy {\n  brand: CaseStudyTabsBrand;\n  /** Featured headline paragraph rendered in large type. Omit to render testimonial-only. */\n  headline?: React.ReactNode;\n  /** Optional \"Read case study\" CTA. */\n  cta?: { label: string; href: string };\n  /** Pull-quote rendered with curly-quote pseudo-elements. */\n  quote: React.ReactNode;\n  author: CaseStudyAuthor;\n}\n\nexport interface CaseStudyTabsProps {\n  cases: CaseStudy[];\n  defaultCaseId?: string;\n  /** Decorative embossed stars rendered above the tab bar. Set to 0 to hide. */\n  starCount?: number;\n  className?: string;\n}\n\n/* ── stars row ───────────────────────────────────────────────── */\n\nfunction StarsRow({ count }: { count: number }) {\n  return (\n    <div className=\"flex gap-1\">\n      {Array.from({ length: count }, (_, i) => (\n        <Star\n          key={i}\n          className=\"size-5 fill-background stroke-background drop-shadow\"\n        />\n      ))}\n    </div>\n  );\n}\n\n/* ── avatar ──────────────────────────────────────────────────── */\n\nfunction AuthorAvatar({ author }: { author: CaseStudyAuthor }) {\n  const frame =\n    \"aspect-square size-10 overflow-hidden rounded-lg border border-transparent shadow-md shadow-black/15 ring-1 ring-foreground/10\";\n\n  if (author.avatarUrl) {\n    return (\n      <div className={frame}>\n        {/* eslint-disable-next-line @next/next/no-img-element */}\n        <img\n          src={author.avatarUrl}\n          alt={`${author.name}'s avatar`}\n          loading=\"lazy\"\n          width={80}\n          height={80}\n          className=\"size-full object-cover\"\n        />\n      </div>\n    );\n  }\n\n  const fallback =\n    author.avatarFallback ?? author.name.trim().charAt(0).toUpperCase();\n\n  return (\n    <div\n      className={cn(\n        frame,\n        \"flex items-center justify-center bg-muted text-sm font-semibold text-muted-foreground\",\n      )}\n    >\n      {fallback}\n    </div>\n  );\n}\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function CaseStudyTabs({\n  cases,\n  defaultCaseId,\n  starCount = 5,\n  className,\n}: CaseStudyTabsProps) {\n  const firstId = cases[0]?.brand.id ?? \"\";\n  const [activeId, setActiveId] = React.useState(defaultCaseId ?? firstId);\n\n  const allTestimonialOnly = cases.every((c) => !c.headline && !c.cta);\n\n  const tabBarRef = React.useRef<HTMLDivElement>(null);\n  const [indicator, setIndicator] = React.useState<{\n    x: number;\n    w: number;\n  } | null>(null);\n\n  const activeCase = cases.find((c) => c.brand.id === activeId) ?? cases[0];\n\n  React.useLayoutEffect(() => {\n    const container = tabBarRef.current;\n    if (!container) return;\n\n    const measure = () => {\n      const activeBtn = container.querySelector<HTMLElement>(\n        '[data-state=\"active\"]',\n      );\n      if (activeBtn) {\n        setIndicator({\n          x: activeBtn.offsetLeft,\n          w: activeBtn.offsetWidth,\n        });\n      }\n    };\n\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(container);\n    return () => observer.disconnect();\n  }, [activeId]);\n\n  if (!activeCase) return null;\n\n  return (\n    <section\n      className={cn(\"w-full bg-background py-16 lg:py-24\", className)}\n      aria-label=\"Customer case studies\"\n    >\n      <div className=\"mx-auto max-w-3xl px-4\">\n        {starCount > 0 && (\n          <div className={cn(allTestimonialOnly && \"flex justify-center\")}>\n            <StarsRow count={starCount} />\n          </div>\n        )}\n\n        <Tabs value={activeId} onValueChange={setActiveId} className=\"gap-0\">\n          <div\n            ref={tabBarRef}\n            className={cn(\n              \"relative my-12 w-fit p-0.5\",\n              allTestimonialOnly ? \"mx-auto\" : \"-ml-6\",\n            )}\n          >\n            {/* fading top + bottom border */}\n            <div\n              aria-hidden\n              className={cn(\n                \"pointer-events-none absolute -inset-y-px border-y border-foreground/10\",\n                allTestimonialOnly\n                  ? \"-inset-x-8 [mask-image:linear-gradient(to_right,_transparent_0%,_black_20%,_black_80%,_transparent_100%)]\"\n                  : \"-left-2 -right-8 [mask-image:linear-gradient(to_right,_transparent_0%,_black_5%,_black_80%,_transparent_100%)]\",\n              )}\n            />\n\n            {/* animated underline indicator */}\n            <span\n              aria-hidden\n              className=\"pointer-events-none absolute bottom-0 h-px rounded-full transition-[transform,width,background-color,opacity] duration-300 ease-out\"\n              style={{\n                transform: indicator\n                  ? `translateX(${indicator.x}px)`\n                  : \"translateX(0px)\",\n                width: indicator ? `${indicator.w}px` : \"0px\",\n                opacity: indicator ? 1 : 0,\n                backgroundColor: activeCase.brand.accentColor ?? \"currentColor\",\n              }}\n            />\n\n            <TabsList className=\"flex h-auto items-center gap-1 rounded-none bg-transparent p-0 text-foreground\">\n              {cases.map((c) => (\n                <TabsTrigger\n                  key={c.brand.id}\n                  value={c.brand.id}\n                  aria-label={c.brand.name}\n                  className={cn(\n                    \"relative flex h-10 items-center rounded px-6\",\n                    \"border-transparent bg-transparent text-foreground shadow-none\",\n                    \"transition-all duration-200 hover:bg-foreground/5\",\n                    \"data-[state=active]:bg-transparent data-[state=active]:shadow-none\",\n                    \"not-last:before:absolute not-last:before:-right-[2.5px] not-last:before:inset-y-0.5 not-last:before:w-px not-last:before:bg-foreground/10\",\n                  )}\n                >\n                  {c.brand.logo}\n                </TabsTrigger>\n              ))}\n            </TabsList>\n          </div>\n\n          {cases.map((c) => {\n            const hasTopBlock = Boolean(c.headline || c.cta);\n            return (\n              <TabsContent key={c.brand.id} value={c.brand.id} className=\"mt-0\">\n                {hasTopBlock && (\n                  <div className=\"space-y-8 border-b pb-12\">\n                    {c.headline && (\n                      <p className=\"text-balance text-xl font-medium text-foreground md:text-2xl md:leading-9\">\n                        {c.headline}\n                      </p>\n                    )}\n                    {c.cta && (\n                      <Link\n                        href={c.cta.href}\n                        className={cn(\n                          \"inline-flex h-8 items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent bg-card pl-3 pr-1.5 text-xs font-medium text-foreground shadow-sm shadow-black/10 ring-1 ring-foreground/10\",\n                          \"transition-all duration-200 hover:bg-muted/50 dark:ring-foreground/15 dark:hover:bg-muted/50\",\n                        )}\n                      >\n                        {c.cta.label}\n                        <ChevronRight\n                          className=\"size-4 scale-90 opacity-50\"\n                          strokeWidth={2.5}\n                        />\n                      </Link>\n                    )}\n                  </div>\n                )}\n\n                <div className={cn(hasTopBlock && \"pt-12\")}>\n                  <p\n                    className={cn(\n                      \"text-balance text-lg text-foreground before:mr-1 before:content-['“'] after:ml-1 after:content-['”']\",\n                      allTestimonialOnly && \"text-center\",\n                    )}\n                  >\n                    {c.quote}\n                  </p>\n                  <div\n                    className={cn(\n                      \"mt-6 flex items-center gap-3\",\n                      allTestimonialOnly && \"justify-center\",\n                    )}\n                  >\n                    <AuthorAvatar author={c.author} />\n                    <div className=\"space-y-px\">\n                      <p className=\"text-sm font-medium text-foreground\">\n                        {c.author.name}\n                      </p>\n                      <p className=\"text-xs text-muted-foreground\">\n                        {c.author.role}\n                      </p>\n                    </div>\n                  </div>\n                </div>\n              </TabsContent>\n            );\n          })}\n        </Tabs>\n      </div>\n    </section>\n  );\n}\n\nexport default CaseStudyTabs;\n",
      "type": "registry:ui",
      "target": "components/ruixen/case-study-tabs.tsx"
    }
  ]
}