{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "showcase-bento",
  "type": "registry:ui",
  "title": "Showcase Bento",
  "description": "An animated showcase bento with hopping team portraits, a count-up stat and rotating testimonials.",
  "files": [
    {
      "path": "src/registry/ui/showcase-bento.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport Image from \"next/image\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useInView,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  Atom,\n  Box,\n  Braces,\n  Component,\n  Flame,\n  Layers,\n  Rocket,\n  Sparkles,\n  Triangle,\n  Wind,\n  Zap,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst ACCENT = \"#0A6EFF\";\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\n\nconst photo = (id: string) => `https://images.unsplash.com/${id}?q=80&w=200&auto=format&fit=crop`;\n\nconst TEAM = [\n  { name: \"Aarav\", src: photo(\"photo-1500648767791-00dcc994a43e\") },\n  { name: \"Kabir\", src: photo(\"photo-1507003211169-0a1dd7228f2d\") },\n  { name: \"Rohan\", src: photo(\"photo-1506794778202-cad84cf45f1d\") },\n  { name: \"Vihaan\", src: photo(\"photo-1531427186611-ecfd6d936c79\") },\n];\n\nconst FRAMEWORKS = [\n  { name: \"Next.js\", icon: Triangle },\n  { name: \"Vite\", icon: Zap },\n  { name: \"Remix\", icon: Layers },\n  { name: \"Astro\", icon: Rocket },\n  { name: \"shadcn/ui\", icon: Box },\n];\n\nconst TECHNOLOGIES = [\n  { name: \"React\", icon: Atom },\n  { name: \"TypeScript\", icon: Braces },\n  { name: \"Tailwind CSS\", icon: Wind },\n  { name: \"Motion\", icon: Sparkles },\n  { name: \"Radix UI\", icon: Component },\n  { name: \"Next.js\", icon: Triangle },\n  { name: \"Firebase\", icon: Flame },\n];\n\n// Placeholder quotes from fictional companies about their own products.\nconst TESTIMONIALS = [\n  {\n    brand: \"Lumen\",\n    quote:\n      \"Since adding the AI assistant to our store, our support load has dropped by nearly 60%. Customers get instant answers about sizing and shipping.\",\n    author: \"Maya Fernandes\",\n    role: \"Founder, Lumen\",\n  },\n  {\n    brand: \"Northwind\",\n    quote:\n      \"The new onboarding flow cut our time-to-first-value in half. Activation is up and our churn conversations are finally about features, not friction.\",\n    author: \"Dev Malhotra\",\n    role: \"Head of Product, Northwind\",\n  },\n  {\n    brand: \"Kestrel\",\n    quote:\n      \"We rebuilt the dashboard in three weeks. It feels faster, reads clearer and our team actually enjoys shipping updates to it now.\",\n    author: \"Priya Nair\",\n    role: \"CTO, Kestrel\",\n  },\n];\n\nconst KEYFRAMES = `\n@keyframes showcase-scroll-x { to { transform: translateX(-50%); } }\n@keyframes showcase-glow { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }\n`;\n\nconst loopClass = \"group-hover/loop:[animation-play-state:paused] motion-reduce:animate-none\";\n\nexport type ShowcaseBentoProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  title?: React.ReactNode;\n  /** The headline number in the stat tile. */\n  stat?: number;\n  statSuffix?: string;\n  statLabel?: React.ReactNode;\n  /** Where \"Chat with Devsloka\" goes. */\n  contactHref?: string;\n};\n\nexport function ShowcaseBento({\n  title = \"Built for Teams That Ship\",\n  stat = 90,\n  statSuffix = \"+\",\n  statLabel = \"Components shipped\",\n  contactHref = \"https://www.devsloka.in/contact\",\n  className,\n  style,\n  ...props\n}: ShowcaseBentoProps) {\n  return (\n    <section\n      data-slot=\"showcase-bento\"\n      style={{ \"--showcase-accent\": ACCENT, ...style } as React.CSSProperties}\n      className={cn(\"@container w-full bg-[#F1EFEC] px-4 py-12 text-neutral-900 sm:px-8 sm:py-16\", className)}\n      {...props}\n    >\n      <style href=\"showcase-bento-keyframes\" precedence=\"default\">\n        {KEYFRAMES}\n      </style>\n      <div className=\"mx-auto max-w-6xl\">\n        <h2 className=\"mb-10 text-4xl font-medium tracking-[-0.04em] text-balance @2xl:text-5xl\">{title}</h2>\n        {/* Container queries: the layout follows the component's width, not the window's. */}\n        <div className=\"grid grid-cols-1 gap-3 @2xl:grid-flow-dense @2xl:grid-cols-2 @4xl:grid-flow-row @4xl:grid-cols-[1fr_1fr_1fr] @4xl:grid-rows-[6.5rem_17rem_6.5rem]\">\n          <TeamTile contactHref={contactHref} />\n          <StatTile value={stat} suffix={statSuffix} label={statLabel} />\n          <FrameworksTile />\n          <TestimonialTile />\n          <TechnologiesTile />\n        </div>\n      </div>\n    </section>\n  );\n}\n\nconst tileClass = \"relative overflow-hidden rounded-3xl bg-white\";\n\n// ── Team: portraits hop to a free neighbouring cell every couple of seconds. ──\n\nconst COLS = 5;\nconst ROWS = 4;\nconst START_CELLS = [7, 11, 13, 17];\nconst HOP_MS = 2200;\n\nfunction neighbours(cell: number) {\n  const row = Math.floor(cell / COLS);\n  const col = cell % COLS;\n  // Portraits stay in the inner columns; the outer ones fade into the tile's edge.\n  return [\n    row > 0 && cell - COLS,\n    row < ROWS - 1 && cell + COLS,\n    col > 1 && cell - 1,\n    col < COLS - 2 && cell + 1,\n  ].filter((value): value is number => value !== false);\n}\n\nfunction TeamTile({ contactHref }: { contactHref: string }) {\n  const ref = useRef<HTMLDivElement>(null);\n  const inView = useInView(ref);\n  const reduceMotion = useReducedMotion();\n  const [cells, setCells] = useState(START_CELLS);\n\n  useEffect(() => {\n    if (!inView || reduceMotion) return;\n    const timer = setInterval(() => {\n      setCells((current) => {\n        const who = Math.floor(Math.random() * current.length);\n        const free = neighbours(current[who]).filter((cell) => !current.includes(cell));\n        if (free.length === 0) return current;\n        const next = [...current];\n        next[who] = free[Math.floor(Math.random() * free.length)];\n        return next;\n      });\n    }, HOP_MS);\n    return () => clearInterval(timer);\n  }, [inView, reduceMotion]);\n\n  return (\n    <article className={cn(tileClass, \"flex flex-col p-6 @2xl:row-span-2 @4xl:row-span-3\")}>\n      <div\n        ref={ref}\n        aria-hidden\n        className=\"-mx-10 grid flex-1 grid-cols-5 place-content-center gap-3 [mask-image:radial-gradient(ellipse_70%_75%_at_50%_45%,black_45%,transparent)]\"\n        style={{ gridTemplateRows: `repeat(${ROWS}, auto)` }}\n      >\n        {Array.from({ length: COLS * ROWS }, (_, cell) => (\n          <span\n            key={cell}\n            className=\"aspect-square rounded-xl border border-black/5 bg-linear-to-b from-white to-neutral-100/80\"\n            style={{ gridColumn: (cell % COLS) + 1, gridRow: Math.floor(cell / COLS) + 1 }}\n          />\n        ))}\n        {TEAM.map((member, i) => (\n          <motion.span\n            key={member.name}\n            layout={!reduceMotion}\n            transition={{ layout: { duration: 0.6, ease: EASE_IN_OUT } }}\n            className=\"relative z-10 aspect-square overflow-hidden rounded-xl bg-neutral-200 shadow-md ring-4 ring-white\"\n            style={{ gridColumn: (cells[i] % COLS) + 1, gridRow: Math.floor(cells[i] / COLS) + 1 }}\n          >\n            <Image src={member.src} alt=\"\" fill sizes=\"96px\" className=\"object-cover\" />\n          </motion.span>\n        ))}\n      </div>\n\n      <p className=\"mt-6 text-base font-medium\">Get to know the makers</p>\n      <a\n        href={contactHref}\n        className=\"mt-3 inline-flex w-fit items-center gap-2.5 rounded-xl bg-neutral-900 py-1.5 pr-4 pl-1.5 text-sm font-medium text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.15)] outline-none transition-[background-color,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-black focus-visible:ring-2 focus-visible:ring-[var(--showcase-accent)] focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:active:scale-100\"\n      >\n        <span className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--showcase-accent)]\">\n          <Image src=\"/favicon-dark.png\" alt=\"\" width={16} height={16} />\n        </span>\n        Chat with Devsloka\n      </a>\n    </article>\n  );\n}\n\n// ── Stat: counts up once in view, over a softly glowing grid. ──\n\nconst GLOW_CELLS = [\n  { cell: 3, delay: 0 },\n  { cell: 12, delay: 1.4 },\n  { cell: 18, delay: 2.6 },\n  { cell: 27, delay: 0.8 },\n  { cell: 33, delay: 3.2 },\n];\n\nfunction StatTile({ value, suffix, label }: { value: number; suffix: string; label: React.ReactNode }) {\n  const numberRef = useRef<HTMLSpanElement>(null);\n  const inView = useInView(numberRef, { once: true, amount: 0.6 });\n  const reduceMotion = useReducedMotion();\n\n  // Writes straight to the DOM, so counting never re-renders React. The server renders\n  // the final value, so it's correct without JavaScript.\n  useEffect(() => {\n    const node = numberRef.current;\n    if (!inView || reduceMotion || !node) return;\n    const controls = animate(0, value, {\n      duration: 1.4,\n      ease: EASE_OUT,\n      onUpdate: (latest) => {\n        node.textContent = String(Math.round(latest));\n      },\n    });\n    return () => controls.stop();\n  }, [inView, reduceMotion, value]);\n\n  return (\n    <article className={cn(tileClass, \"flex min-h-72 flex-col justify-between p-7 @4xl:row-span-2 @4xl:min-h-0\")}>\n      <div aria-hidden className=\"absolute inset-0 grid grid-cols-6 grid-rows-6 [mask-image:radial-gradient(ellipse_80%_70%_at_70%_30%,black,transparent)]\">\n        {Array.from({ length: 36 }, (_, cell) => {\n          const glow = GLOW_CELLS.find((item) => item.cell === cell);\n          return (\n            <span key={cell} className=\"relative border-r border-b border-black/[0.04]\">\n              {glow && (\n                <span\n                  className=\"absolute inset-0 bg-[color-mix(in_oklab,var(--showcase-accent)_10%,transparent)] opacity-0 animate-[showcase-glow_4.5s_ease-in-out_infinite] motion-reduce:animate-none\"\n                  style={{ animationDelay: `${glow.delay}s` }}\n                />\n              )}\n            </span>\n          );\n        })}\n      </div>\n\n      <div className=\"relative\">\n        <p className=\"text-7xl leading-none font-medium tracking-[-0.06em] tabular-nums @4xl:text-8xl\">\n          <span ref={numberRef}>{value}</span>\n          {suffix}\n        </p>\n        <p className=\"mt-4 text-base text-neutral-500\">{label}</p>\n      </div>\n      <p className=\"relative mt-10 max-w-xs text-sm leading-relaxed text-neutral-500\">\n        Animated, accessible React components you copy into your project and own. No lock-in. Just\n        source.\n      </p>\n    </article>\n  );\n}\n\n// ── Frameworks: a drifting row of chips. ──\n\nfunction FrameworksTile() {\n  return (\n    <article\n      aria-label=\"Works with\"\n      className={cn(\n        tileClass,\n        // Full width in the two-column layout, so the testimonial can sit beside the team.\n        \"group/loop flex min-h-24 items-center @2xl:col-span-2 @4xl:col-span-1 [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]\"\n      )}\n    >\n      <ul className={cn(\"flex w-max gap-3 pr-3 animate-[showcase-scroll-x_22s_linear_infinite]\", loopClass)}>\n        {[...FRAMEWORKS, ...FRAMEWORKS, ...FRAMEWORKS, ...FRAMEWORKS].map(({ name, icon: Icon }, i) => (\n          <li\n            key={i}\n            aria-hidden={i >= FRAMEWORKS.length}\n            className=\"flex shrink-0 items-center gap-2 rounded-lg bg-white py-1.5 pr-3.5 pl-1.5 text-sm shadow-[0_1px_3px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.04)]\"\n          >\n            <span className=\"flex size-7 items-center justify-center rounded-md bg-neutral-900 text-white\">\n              <Icon aria-hidden className=\"size-3.5\" />\n            </span>\n            {name}\n          </li>\n        ))}\n      </ul>\n    </article>\n  );\n}\n\n// ── Testimonial: rotates with a soft blur crossfade; dots pick one directly. ──\n\nconst ROTATE_MS = 6000;\n\nfunction TestimonialTile() {\n  const ref = useRef<HTMLElement>(null);\n  const inView = useInView(ref);\n  const reduceMotion = useReducedMotion();\n  const [index, setIndex] = useState(0);\n  const [hovered, setHovered] = useState(false);\n\n  useEffect(() => {\n    if (!inView || hovered || reduceMotion) return;\n    const timer = setTimeout(() => setIndex((i) => (i + 1) % TESTIMONIALS.length), ROTATE_MS);\n    return () => clearTimeout(timer);\n  }, [index, inView, hovered, reduceMotion]);\n\n  const item = TESTIMONIALS[index];\n\n  return (\n    <article\n      ref={ref}\n      onPointerEnter={() => setHovered(true)}\n      onPointerLeave={() => setHovered(false)}\n      className={cn(tileClass, \"flex min-h-72 flex-col p-7 @4xl:min-h-0\")}\n    >\n      {/* Soft lit squares in the corner, echoing the stat tile's grid. */}\n      <div aria-hidden className=\"pointer-events-none absolute -top-6 right-0 grid w-56 grid-cols-3 gap-0 opacity-80 blur-[1px]\">\n        {[0.5, 0.9, 0.35, 0.25, 1, 0.6, 0.15, 0.4, 0.2].map((strength, i) => (\n          <span\n            key={i}\n            className=\"aspect-square bg-linear-to-b from-[color-mix(in_oklab,var(--showcase-accent)_14%,white)] to-transparent\"\n            style={{ opacity: strength }}\n          />\n        ))}\n      </div>\n\n      <div aria-live=\"polite\" className=\"relative mt-auto grid\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.figure\n            key={index}\n            initial={reduceMotion ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\", transform: \"translateY(6px)\" }}\n            animate={{ opacity: 1, filter: \"blur(0px)\", transform: \"translateY(0px)\" }}\n            exit={reduceMotion ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\", transform: \"translateY(-6px)\" }}\n            transition={{ duration: 0.4, ease: EASE_OUT }}\n            className=\"[grid-area:1/1]\"\n          >\n            <p className=\"text-2xl font-semibold tracking-tight\">{item.brand}</p>\n            <blockquote className=\"mt-4 text-sm leading-relaxed text-neutral-500\">&ldquo;{item.quote}&rdquo;</blockquote>\n            <figcaption className=\"mt-5 text-sm\">\n              <span className=\"font-medium\">— {item.author}</span>{\" \"}\n              <span className=\"text-neutral-500\">{item.role}</span>\n            </figcaption>\n          </motion.figure>\n        </AnimatePresence>\n      </div>\n\n      <div className=\"relative mt-5 flex gap-1.5\">\n        {TESTIMONIALS.map((testimonial, i) => (\n          <button\n            key={testimonial.brand}\n            type=\"button\"\n            onClick={() => setIndex(i)}\n            aria-label={`Show testimonial from ${testimonial.brand}`}\n            aria-current={i === index ? \"true\" : undefined}\n            className=\"group/dot flex h-4 cursor-pointer items-center outline-none\"\n          >\n            <span\n              className={cn(\n                \"block h-1 rounded-full transition-[width,background-color] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] group-focus-visible/dot:ring-2 group-focus-visible/dot:ring-[var(--showcase-accent)]\",\n                i === index ? \"w-5 bg-neutral-900\" : \"w-1.5 bg-neutral-300 group-hover/dot:bg-neutral-400\"\n              )}\n            />\n          </button>\n        ))}\n      </div>\n    </article>\n  );\n}\n\n// ── Technologies: a drifting row of icon tiles. ──\n\nfunction TechnologiesTile() {\n  return (\n    <article className={cn(tileClass, \"flex items-center gap-6 py-5 pl-7 @2xl:col-span-2\")}>\n      <p className=\"shrink-0 text-base font-medium\">Technologies we use</p>\n      {/* Vertical padding (offset by a negative margin) keeps the tiles' outline and shadow\n          inside the clipping box, so their top edge isn't cut off. */}\n      <div className=\"group/loop -my-3 min-w-0 flex-1 overflow-hidden py-3 [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]\">\n        <ul aria-label=\"Technologies\" className={cn(\"flex w-max gap-4 pr-4 animate-[showcase-scroll-x_26s_linear_infinite]\", loopClass)}>\n          {[...TECHNOLOGIES, ...TECHNOLOGIES].map(({ name, icon: Icon }, i) => (\n            <li\n              key={i}\n              aria-hidden={i >= TECHNOLOGIES.length}\n              title={name}\n              className=\"flex size-12 shrink-0 items-center justify-center rounded-xl bg-white shadow-[0_1px_3px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.04)]\"\n            >\n              <Icon aria-hidden className=\"size-5\" />\n              <span className=\"sr-only\">{name}</span>\n            </li>\n          ))}\n        </ul>\n      </div>\n    </article>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ]
}
