{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-cinemagraph",
  "type": "registry:block",
  "title": "Cinemagraph micro-motion hero",
  "description": "One still photo with a single living region (sway, breathe or glint) isolated by a soft mask, no video file, with a pause control.",
  "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-cinemagraph.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { motion, useInView, useReducedMotion } from \"motion/react\"\nimport { Pause, Play } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Cinemagraph micro-motion hero. One still photograph, one living detail.\n   No video file: a soft-edged mask isolates a `region` of the same image and\n   only that region moves (a slow sway, a breath or a passing glint), so the\n   scene reads as a photo with a single loop in it. The loop stops when the\n   hero leaves the viewport, a pause control satisfies WCAG 2.2.2, and with\n   reduced motion the image is simply still. Copy sits on a paper-token fade\n   over the quiet side of the frame on lg and stacks below the image on\n   mobile. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\ntype Cta = { label: string; href: string }\ntype Region = { x: number; y: number; rx: number; ry: number }\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\nexport function HeroCinemagraph({\n  image = \"/placeholder.webp\",\n  imageAlt = \"\",\n  region = { x: 44, y: 30, rx: 14, ry: 28 },\n  effect = \"sway\",\n  period = 7,\n  focus = \"70% center\",\n  eyebrow = \"Slow mornings\",\n  headline = \"The quiet part of your routine.\",\n  subcopy = \"One scoop, a glass of water and a minute of sun. Daily essentials that fit the way you already start the day.\",\n  primaryCta = { label: \"Shop the routine\", href: \"#\" },\n  secondaryCta = { label: \"Our ingredients\", href: \"#\" },\n  className,\n}: {\n  image?: string\n  imageAlt?: string\n  /** The living area, as an ellipse in percent of the frame. */\n  region?: Region\n  /** sway: drifting shadows or leaves. breathe: steam, fabric, water. glint: light passing over glass or foil. */\n  effect?: \"sway\" | \"breathe\" | \"glint\"\n  /** Seconds per loop. Slower reads as calmer. */\n  period?: number\n  /** object-position for the photo, so the subject stays in frame on narrow screens. */\n  focus?: string\n  eyebrow?: string\n  headline?: string\n  subcopy?: string\n  primaryCta?: Cta\n  secondaryCta?: Cta\n  className?: string\n}) {\n  const prefersReduced = useReducedMotion()\n  // Server HTML assumes motion; read the preference only after hydration so markup matches.\n  const [hydrated, setHydrated] = useState(false)\n  useEffect(() => setHydrated(true), [])\n  const reduce = hydrated && !!prefersReduced\n  const sk = useSkBlock(\"hero\", \"hero-cinemagraph\")\n  const frame = useRef<HTMLDivElement>(null)\n  const inView = useInView(frame, { amount: 0.2 })\n  const [paused, setPaused] = useState(false)\n  const live = !reduce && !paused && inView\n\n  const mask = `radial-gradient(ellipse ${region.rx}% ${region.ry}% at ${region.x}% ${region.y}%, rgb(0 0 0) 35%, transparent 100%)`\n  const loop = { duration: period, ease: \"easeInOut\" as const, repeat: Infinity, repeatType: \"mirror\" as const }\n  const motionFor =\n    effect === \"sway\"\n      ? { animate: live ? { x: [0, 7, -3], skewX: [0, 0.8, -0.4] } : { x: 0, skewX: 0 } }\n      : effect === \"breathe\"\n        ? { animate: live ? { scale: [1, 1.025, 1.01], y: [0, -4, 0] } : { scale: 1, y: 0 } }\n        : { animate: { x: 0 } }\n\n  const rise = (i: number) => ({\n    initial: { opacity: 0, y: reduce ? 0 : 8 },\n    animate: { opacity: 1, y: 0 },\n    transition: { duration: 0.5, ease: EASE, delay: reduce ? 0 : 0.1 + i * 0.06 },\n  })\n\n  return (\n    <section ref={sk.ref} {...sk.attrs} className={cn(\"relative isolate overflow-hidden bg-sk-paper text-sk-ink\", className)}>\n      <div ref={frame} className=\"relative aspect-[4/3] w-full sm:aspect-[16/9] lg:absolute lg:inset-0 lg:-z-10 lg:aspect-auto\">\n        <img src={image} alt={imageAlt} className=\"absolute inset-0 h-full w-full object-cover\" style={{ objectPosition: focus }} />\n        {!reduce ? (\n          <div aria-hidden className=\"pointer-events-none absolute inset-0 overflow-hidden\" style={{ maskImage: mask, WebkitMaskImage: mask }}>\n            {effect === \"glint\" ? (\n              <motion.div\n                className=\"absolute inset-y-0 -left-1/3 w-1/3 bg-[linear-gradient(100deg,transparent,rgb(255_255_255/0.38),transparent)] mix-blend-soft-light\"\n                animate={live ? { x: [\"0%\", \"400%\"] } : undefined}\n                transition={{ duration: period, ease: \"easeInOut\", repeat: Infinity, repeatDelay: period * 0.4 }}\n              />\n            ) : (\n              <motion.img\n                src={image}\n                alt=\"\"\n                {...motionFor}\n                transition={loop}\n                className=\"absolute inset-0 h-full w-full object-cover will-change-transform\"\n                style={{ objectPosition: focus, transformOrigin: `${region.x}% ${region.y + region.ry}%` }}\n              />\n            )}\n          </div>\n        ) : null}\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 hidden bg-[linear-gradient(90deg,rgb(var(--sk-paper)/0.94),rgb(var(--sk-paper)/0.72)_34%,rgb(var(--sk-paper)/0)_58%)] lg:block\"\n        />\n        {!reduce ? (\n          <button\n            type=\"button\"\n            onClick={() => {\n              setPaused((p) => !p)\n              sk.emit(\"interact\", paused ? \"motion_play\" : \"motion_pause\")\n            }}\n            aria-label={paused ? \"Play background motion\" : \"Pause background motion\"}\n            aria-pressed={paused}\n            className=\"absolute bottom-3 right-3 grid h-11 w-11 place-items-center rounded-full bg-[rgb(0_0_0/0.45)] text-[rgb(255_255_255)] outline-none backdrop-blur-sm transition-[background-color,transform] duration-200 ease-sk-out hover:bg-[rgb(0_0_0/0.6)] focus-visible:ring-2 focus-visible:ring-[rgb(255_255_255)] active:scale-[0.97] lg:bottom-6 lg:right-6\"\n          >\n            {paused ? <Play size={15} aria-hidden className=\"translate-x-px\" /> : <Pause size={15} aria-hidden />}\n          </button>\n        ) : null}\n      </div>\n\n      <div className=\"mx-auto flex w-full max-w-6xl px-4 py-10 sm:px-8 sm:py-12 lg:min-h-[600px] lg:items-center lg:py-20\">\n        <div className=\"max-w-md\">\n          <motion.p {...rise(0)} className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">\n            {eyebrow}\n          </motion.p>\n          <motion.h1\n            {...rise(1)}\n            className=\"mt-4 text-balance font-sk-display font-semibold leading-[1.06] tracking-[-0.02em]\"\n            style={{ fontSize: \"clamp(2.25rem, 4.6vw, 3.75rem)\" }}\n          >\n            {headline}\n          </motion.h1>\n          <motion.p {...rise(2)} className=\"mt-5 text-[15px] leading-[1.55] text-sk-muted\">\n            {subcopy}\n          </motion.p>\n          <motion.div {...rise(3)} className=\"mt-8 flex flex-col gap-3 sm: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          </motion.div>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/hero/hero-cinemagraph.tsx"
    }
  ],
  "docs": "Set `region` over the area that should move (shadows, steam, liquid). Unlike hero-video it needs no clip; it stops offscreen and under reduced motion.",
  "meta": {
    "section": "hero",
    "variant": "cinemagraph",
    "niche": [
      "any"
    ],
    "commerce": [
      "cta"
    ],
    "previewHeight": 620
  }
}