{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-ugc",
  "type": "registry:block",
  "title": "UGC customer hero",
  "description": "Customer photos or vertical clips lead: a tall featured post and two supporting posts with handle, verified mark, quote and shop link.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "hero",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "hero/hero-ugc.tsx",
      "content": "\"use client\"\n\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { ArrowUpRight, BadgeCheck } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* UGC customer hero. Real customer photos or vertical clips carry the hero:\n   one tall featured post and two supporting posts, each with the customer's\n   handle, a verified buyer mark, a short quote and a link to the product in\n   the shot. Copy stays brief so the posts do the selling. Captions sit on a\n   fixed dark scrim so they read on any photo. On mobile the featured post\n   leads, the copy follows and the supporting posts sit side by side. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\ntype Cta = { label: string; href: string }\nexport type UgcPost = {\n  image: string\n  /** Optional short vertical clip; the image is its poster. Muted, looped, off under reduced motion. */\n  video?: string\n  alt: string\n  handle: string\n  quote?: string\n  product?: { title: string; href: string }\n  verified?: boolean\n  /** CSS object-position for the crop. */\n  focus?: string\n}\n\nconst PRIMARY =\n  \"sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn bg-sk-accent px-7 text-[15px] font-medium text-sk-accent-ink outline-none transition-[filter,transform] duration-200 ease-sk-out hover:brightness-[1.06] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]\"\nconst SECONDARY =\n  \"sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn border border-sk-line px-7 text-[15px] font-medium text-sk-ink outline-none transition-[background-color,transform] duration-200 ease-sk-out hover:bg-sk-ink/[0.04] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]\"\n\nconst SAMPLE: UgcPost[] = [\n  { image: \"/placeholder.webp\", alt: \"Customer photo\", handle: \"@maya.moves\", quote: \"Two months in and I actually look forward to my morning glass.\", product: { title: \"Daily Greens\", href: \"#\" }, verified: true },\n  { image: \"/placeholder.webp\", alt: \"Customer photo\", handle: \"@coachdre\", quote: \"Packed in my gym bag every day.\", product: { title: \"Electrolytes\", href: \"#\" }, verified: true },\n  { image: \"/placeholder.webp\", alt: \"Customer photo\", handle: \"@sam.slowmornings\", quote: \"The whole shelf, honestly.\", product: { title: \"Shop the set\", href: \"#\" }, verified: true },\n]\n\nexport function HeroUgc({\n  eyebrow = \"Shot by customers, not studios\",\n  headline = \"Real routines. Real kitchens.\",\n  subcopy = \"Thousands of customers share how they take theirs. Tag us to be featured.\",\n  primaryCta = { label: \"Shop customer favorites\", href: \"#\" },\n  secondaryCta = { label: \"See all posts\", href: \"#\" },\n  posts = SAMPLE,\n  stat = { value: \"12,400+\", label: \"customer posts\" },\n  className,\n}: {\n  eyebrow?: string\n  headline?: string\n  subcopy?: string\n  primaryCta?: Cta\n  secondaryCta?: Cta\n  /** First post is featured. Up to three are shown. */\n  posts?: UgcPost[]\n  stat?: { value: string; label: string } | null\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"hero\", \"hero-ugc\")\n  const [lead, ...rest] = posts\n  const side = rest.slice(0, 2)\n\n  const tile = (post: UgcPost, i: number, big?: boolean) => (\n    <motion.figure\n      initial={{ opacity: 0, y: reduce ? 0 : 12 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.55, ease: EASE, delay: reduce ? 0 : 0.08 * i }}\n      className={cn(\n        \"group relative overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface\",\n        big ? \"aspect-[4/5] lg:aspect-auto\" : \"aspect-[4/5] lg:aspect-auto\"\n      )}\n    >\n      {post.video && !reduce ? (\n        <video src={post.video} poster={post.image} muted loop playsInline autoPlay aria-label={post.alt} className=\"absolute inset-0 h-full w-full object-cover\" style={{ objectPosition: post.focus }} />\n      ) : (\n        <img src={post.image} alt={post.alt} className=\"absolute inset-0 h-full w-full object-cover transition-transform duration-700 ease-sk-out group-hover:scale-[1.03]\" style={{ objectPosition: post.focus }} />\n      )}\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 bg-[linear-gradient(0deg,rgb(0_0_0/0.72),rgb(0_0_0/0.2)_45%,transparent_70%)]\" />\n      <figcaption className={cn(\"absolute inset-x-0 bottom-0 text-[rgb(255_255_255)]\", big ? \"p-4 sm:p-5\" : \"p-3 sm:p-4\")}>\n        <p className=\"flex min-w-0 items-center gap-1.5 text-[12px] font-medium sm:text-[13px]\">\n          <span className=\"truncate\">{post.handle}</span>\n          {post.verified ? (\n            <>\n              <BadgeCheck size={14} aria-hidden className=\"opacity-90\" />\n              <span className=\"sr-only\">Verified buyer</span>\n            </>\n          ) : null}\n        </p>\n        {post.quote ? (\n          <blockquote className={cn(\"mt-1.5 text-pretty text-[rgb(255_255_255/0.88)]\", big ? \"text-[15px] sm:text-base\" : \"hidden text-[13px] sm:block\", \"leading-[1.4]\")}>\n            <span className={big ? undefined : \"line-clamp-2\"}>{post.quote}</span>\n          </blockquote>\n        ) : null}\n        {post.product ? (\n          <a\n            href={post.product.href}\n            onClick={() => sk.emit(\"interact\", \"ugc_product\", i)}\n            className=\"mt-3 inline-flex min-h-9 items-center gap-1 rounded-sk-chip bg-[rgb(255_255_255/0.16)] px-3 text-[12px] font-medium text-[rgb(255_255_255)] outline-none backdrop-blur-md transition-[background-color,transform] duration-200 ease-sk-out hover:bg-[rgb(255_255_255/0.26)] focus-visible:ring-2 focus-visible:ring-[rgb(255_255_255)] active:scale-[0.97]\"\n          >\n            {post.product.title}\n            <ArrowUpRight size={13} aria-hidden />\n          </a>\n        ) : null}\n      </figcaption>\n    </motion.figure>\n  )\n\n  return (\n    <section ref={sk.ref} {...sk.attrs} className={cn(\"bg-sk-paper text-sk-ink\", className)}>\n      <div className=\"mx-auto grid w-full max-w-6xl gap-8 px-4 py-10 sm:px-8 sm:py-14 lg:grid-cols-[0.8fr_1.2fr] lg:items-center lg:gap-12\">\n        <motion.div\n          initial={{ opacity: 0, y: reduce ? 0 : 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.5, ease: EASE }}\n          className=\"order-2 lg:order-1\"\n        >\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{eyebrow}</p>\n          <h1 className=\"mt-4 text-balance font-sk-display font-semibold leading-[1.06] tracking-[-0.02em]\" style={{ fontSize: \"clamp(2.25rem, 4.4vw, 3.5rem)\" }}>\n            {headline}\n          </h1>\n          <p className=\"mt-5 max-w-md text-[15px] leading-[1.55] text-sk-muted\">{subcopy}</p>\n          <div className=\"mt-8 flex flex-col gap-3 sm:flex-row lg:flex-col xl:flex-row\">\n            <a href={primaryCta.href} onClick={() => sk.emit(\"interact\", \"cta\")} className={PRIMARY}>\n              {primaryCta.label}\n            </a>\n            <a href={secondaryCta.href} onClick={() => sk.emit(\"interact\", \"cta_secondary\")} className={SECONDARY}>\n              {secondaryCta.label}\n            </a>\n          </div>\n          {stat ? (\n            <p className=\"mt-8 flex items-baseline gap-2 border-t border-sk-line pt-5\">\n              <span className=\"font-sk-display text-2xl font-semibold tabular-nums\">{stat.value}</span>\n              <span className=\"text-sm text-sk-muted\">{stat.label}</span>\n            </p>\n          ) : null}\n        </motion.div>\n\n        <div className=\"order-1 grid grid-cols-2 gap-3 lg:order-2 lg:h-[560px] lg:grid-rows-2 lg:gap-4\">\n          {lead ? (\n            <div className=\"col-span-2 lg:col-span-1 lg:row-span-2 lg:grid\">\n              {tile(lead, 0, true)}\n            </div>\n          ) : null}\n          {side.map((p, i) => (\n            <div key={p.handle + i} className=\"lg:grid\">\n              {tile(p, i + 1)}\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/hero/hero-ugc.tsx"
    }
  ],
  "docs": "Use posts you have rights to. A post's `video` plays muted and is replaced by its image under reduced motion.",
  "meta": {
    "section": "hero",
    "variant": "ugc",
    "niche": [
      "any"
    ],
    "commerce": [
      "cta"
    ],
    "previewHeight": 660
  }
}