{"version":1,"id":"starter-board","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/starter-board.tsx","content":"\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport Image from \"next/image\";\nimport { Check, Sparkles } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type StarterBoardTrust = {\n  label: string;\n  icon?: \"check\" | \"sparkles\";\n};\n\nexport type StarterBoardSlide = {\n  id: string;\n  imageSrc: string;\n  imageAlt: string;\n  title: string;\n  lead?: string;\n  features: string[];\n  ctaLabel: string;\n  trusts: StarterBoardTrust[];\n};\n\nconst AUTO_MS = 6500;\n\nexport const STARTER_BOARD_SLIDES: StarterBoardSlide[] = [\n  {\n    id: \"start\",\n    imageSrc: \"/starter-board/grass-a.png\",\n    imageAlt: \"Abstract green motion field\",\n    title: \"Your membership starts here\",\n    lead: \"Everything you need to go from first setup to a living health baseline.\",\n    features: [\n      \"Annual 100+ biomarker panel\",\n      \"Data dashboard and digital twin\",\n      \"Upload past labs and connect wearables\",\n      \"Personalized health protocol\",\n      \"24/7 care team access\",\n      \"AI companion for all health questions\",\n      \"Marketplace with additional solutions\",\n    ],\n    ctaLabel: \"Get started\",\n    trusts: [\n      { label: \"Takes a few minutes\", icon: \"check\" },\n      { label: \"No card required\", icon: \"check\" },\n      { label: \"Guided setup\", icon: \"sparkles\" },\n    ],\n  },\n  {\n    id: \"baseline\",\n    imageSrc: \"/starter-board/grass-b.png\",\n    imageAlt: \"Soft green grass in motion\",\n    title: \"Build your baseline first\",\n    lead: \"Start with clarity — labs, history, and wearables in one place.\",\n    features: [\n      \"Import past labs in minutes\",\n      \"Connect Apple Watch, Oura, and more\",\n      \"See trends instead of one-off numbers\",\n      \"Plain-language biomarker ranges\",\n      \"Flags when something needs a closer look\",\n      \"Share a clean summary with your doctor\",\n      \"Reminders when it’s time to retest\",\n    ],\n    ctaLabel: \"Get started\",\n    trusts: [\n      { label: \"Private by default\", icon: \"check\" },\n      { label: \"Cancel anytime\", icon: \"check\" },\n      { label: \"Human + AI help\", icon: \"sparkles\" },\n    ],\n  },\n  {\n    id: \"protocol\",\n    imageSrc: \"/starter-board/grass-c.png\",\n    imageAlt: \"Blurred green landscape streaks\",\n    title: \"Turn insights into a protocol\",\n    lead: \"A plan ranked by impact for you — light enough to keep, strong enough to move.\",\n    features: [\n      \"Actions ordered by expected impact\",\n      \"Nutrition and training suggestions\",\n      \"Sleep and recovery targets\",\n      \"Supplements only when they earn a place\",\n      \"Weekly check-ins that stay short\",\n      \"Adjustments as your markers shift\",\n      \"Optional clinician review\",\n    ],\n    ctaLabel: \"Get started\",\n    trusts: [\n      { label: \"Evidence-informed\", icon: \"check\" },\n      { label: \"Editable anytime\", icon: \"check\" },\n      { label: \"Built around you\", icon: \"sparkles\" },\n    ],\n  },\n  {\n    id: \"care\",\n    imageSrc: \"/starter-board/grass-d.png\",\n    imageAlt: \"Diagonal green light streaks\",\n    title: \"Stay supported after day one\",\n    lead: \"Questions, check-ins, and escalation — without another app to manage.\",\n    features: [\n      \"24/7 messaging with the care team\",\n      \"AI companion for everyday questions\",\n      \"Escalation to a clinician when needed\",\n      \"Wearable context in every conversation\",\n      \"Optional marketplace add-ons\",\n      \"Invite family when you’re ready\",\n      \"Your data stays yours\",\n    ],\n    ctaLabel: \"Get started\",\n    trusts: [\n      { label: \"Real humans on call\", icon: \"check\" },\n      { label: \"Cancel anytime\", icon: \"check\" },\n      { label: \"Always-on companion\", icon: \"sparkles\" },\n    ],\n  },\n];\n\nexport type StarterBoardProps = {\n  slides?: StarterBoardSlide[];\n  autoPlayMs?: number;\n  className?: string;\n  onCtaClick?: (slide: StarterBoardSlide) => void;\n};\n\nexport function StarterBoard({\n  slides = STARTER_BOARD_SLIDES,\n  autoPlayMs = AUTO_MS,\n  className,\n  onCtaClick,\n}: StarterBoardProps) {\n  const count = slides.length;\n  const [index, setIndex] = useState(0);\n  const [progress, setProgress] = useState(0);\n  const [paused, setPaused] = useState(false);\n  const reduceMotionRef = useRef(false);\n\n  const goTo = useCallback(\n    (next: number) => {\n      if (count === 0) return;\n      const wrapped = ((next % count) + count) % count;\n      setIndex(wrapped);\n      setProgress(0);\n    },\n    [count],\n  );\n\n  useEffect(() => {\n    reduceMotionRef.current = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    ).matches;\n  }, []);\n\n  useEffect(() => {\n    if (paused || count < 2 || autoPlayMs <= 0) return;\n    if (reduceMotionRef.current) return;\n\n    let raf = 0;\n    let start = performance.now();\n    const tick = (now: number) => {\n      const t = Math.min(1, (now - start) / autoPlayMs);\n      setProgress(t);\n      if (t >= 1) {\n        start = now;\n        setIndex((i) => (i + 1) % count);\n        setProgress(0);\n      }\n      raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [paused, count, autoPlayMs, index]);\n\n  const slide = slides[index];\n  if (!slide) return null;\n\n  return (\n    <section\n      data-slot=\"starter-board\"\n      className={cn(\n        \"w-full max-w-[64rem] overflow-hidden rounded-[1.75rem] bg-[#f4f4f5] text-zinc-950\",\n        \"dark:bg-zinc-900 dark:text-zinc-50\",\n        className,\n      )}\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocusCapture={() => setPaused(true)}\n      onBlurCapture={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {\n          setPaused(false);\n        }\n      }}\n      aria-roledescription=\"carousel\"\n      aria-label=\"Starter board\"\n    >\n      <div className=\"grid gap-8 p-5 md:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)] md:gap-10 md:p-7 lg:p-8\">\n        <div className=\"flex min-w-0 flex-col\">\n          <div className=\"relative aspect-square w-full overflow-hidden rounded-2xl bg-zinc-200 dark:bg-zinc-800\">\n            {slides.map((s, i) => (\n              <div\n                key={s.id}\n                className={cn(\n                  \"absolute inset-0 transition-opacity duration-500 ease-out\",\n                  i === index ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n                )}\n                aria-hidden={i !== index}\n              >\n                <Image\n                  src={s.imageSrc}\n                  alt={s.imageAlt}\n                  fill\n                  sizes=\"(max-width: 768px) 100vw, 32rem\"\n                  className=\"object-cover\"\n                  priority={i === 0}\n                />\n              </div>\n            ))}\n          </div>\n\n          <div className=\"mt-4 flex items-center gap-3 px-0.5\">\n            <div\n              className=\"flex flex-1 items-center gap-1.5\"\n              role=\"tablist\"\n              aria-label=\"Slides\"\n            >\n              {slides.map((s, i) => {\n                const active = i === index;\n                return (\n                  <button\n                    key={s.id}\n                    type=\"button\"\n                    role=\"tab\"\n                    aria-selected={active}\n                    aria-label={`Slide ${i + 1} of ${count}: ${s.title}`}\n                    onClick={() => goTo(i)}\n                    className={cn(\n                      \"relative h-1 overflow-hidden rounded-full transition-all duration-300\",\n                      active\n                        ? \"flex-[2.4] bg-zinc-300 dark:bg-zinc-700\"\n                        : \"flex-1 bg-zinc-300 hover:bg-zinc-400 dark:bg-zinc-700 dark:hover:bg-zinc-600\",\n                    )}\n                  >\n                    {active ? (\n                      <span\n                        className=\"absolute inset-y-0 left-0 rounded-full bg-zinc-950 dark:bg-zinc-50\"\n                        style={{\n                          width: `${Math.max(progress, 0.04) * 100}%`,\n                        }}\n                      />\n                    ) : null}\n                  </button>\n                );\n              })}\n            </div>\n            <span\n              className=\"shrink-0 tabular-nums text-[13px] font-medium tracking-tight text-zinc-500 dark:text-zinc-400\"\n              aria-live=\"polite\"\n            >\n              {index + 1} / {count}\n            </span>\n          </div>\n        </div>\n\n        <div\n          key={slide.id}\n          className=\"flex min-w-0 flex-col justify-center md:py-2\"\n        >\n          <h2 className=\"text-[1.75rem] font-semibold leading-[1.15] tracking-tight text-zinc-950 sm:text-[2rem] dark:text-zinc-50\">\n            {slide.title}\n          </h2>\n          {slide.lead ? (\n            <p className=\"mt-3 text-[15px] leading-relaxed text-zinc-500 dark:text-zinc-400\">\n              {slide.lead}\n            </p>\n          ) : null}\n\n          <ul className=\"mt-5 space-y-2.5\">\n            {slide.features.map((item) => (\n              <li\n                key={item}\n                className=\"flex items-start gap-2.5 text-[15px] leading-snug text-zinc-800 dark:text-zinc-200\"\n              >\n                <span\n                  className=\"mt-[0.45rem] size-1.5 shrink-0 rounded-full bg-zinc-400 dark:bg-zinc-500\"\n                  aria-hidden\n                />\n                <span>{item}</span>\n              </li>\n            ))}\n          </ul>\n\n          <button\n            type=\"button\"\n            onClick={() => onCtaClick?.(slide)}\n            className={cn(\n              \"mt-8 inline-flex h-12 w-full items-center justify-center rounded-full\",\n              \"bg-zinc-950 text-[15px] font-medium text-white transition hover:bg-zinc-800\",\n              \"dark:bg-zinc-50 dark:text-zinc-950 dark:hover:bg-zinc-200\",\n            )}\n          >\n            {slide.ctaLabel}\n          </button>\n\n          <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-[12px] text-zinc-500 dark:text-zinc-400\">\n            {slide.trusts.map((t) => (\n              <span key={t.label} className=\"inline-flex items-center gap-1.5\">\n                {t.icon === \"sparkles\" ? (\n                  <Sparkles\n                    className=\"size-3.5 shrink-0 opacity-70\"\n                    aria-hidden\n                  />\n                ) : (\n                  <Check\n                    className=\"size-3.5 shrink-0 opacity-70\"\n                    strokeWidth={2.5}\n                    aria-hidden\n                  />\n                )}\n                {t.label}\n              </span>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}