{"version":1,"id":"customer-stories-carousel","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/customer-stories-carousel.tsx","content":"\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type PointerEvent as ReactPointerEvent,\n  type MouseEvent as ReactMouseEvent,\n} from \"react\";\nimport Image from \"next/image\";\nimport { ArrowLeftIcon, ArrowRightIcon, PlusIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CustomerStory = {\n  id: string;\n  brand: string;\n  imageSrc: string;\n  imageAlt: string;\n  tags: string[];\n  href?: string;\n};\n\nconst STORIES: CustomerStory[] = [\n  {\n    id: \"velocity\",\n    brand: \"Velocity\",\n    imageSrc:\n      \"https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80\",\n    imageAlt: \"Open office with people working at desks\",\n    tags: [\"Technology\", \"AI\", \"Enterprise\"],\n  },\n  {\n    id: \"vortex\",\n    brand: \"Vortex\",\n    imageSrc:\n      \"https://images.unsplash.com/photo-1559839734-2b71ea197ec2?auto=format&fit=crop&w=1200&q=80\",\n    imageAlt: \"Doctor in a white coat smiling\",\n    tags: [\"Healthcare\", \"Technology\", \"Enterprise\"],\n  },\n  {\n    id: \"synergy\",\n    brand: \"Synergy\",\n    imageSrc:\n      \"https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=1200&q=80\",\n    imageAlt: \"Rows of plastic bottles in production\",\n    tags: [\"Manufacturing\", \"Sustainability\", \"B2B\"],\n  },\n  {\n    id: \"enigma\",\n    brand: \"Enigma\",\n    imageSrc:\n      \"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1200&q=80\",\n    imageAlt: \"Modern glass skyscraper facade\",\n    tags: [\"Fintech\", \"Technology\", \"Enterprise\"],\n  },\n  {\n    id: \"pulse\",\n    brand: \"Pulse\",\n    imageSrc:\n      \"https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&w=1200&q=80\",\n    imageAlt: \"Team collaborating around a laptop\",\n    tags: [\"SaaS\", \"Product\", \"Growth\"],\n  },\n];\n\nfunction BrandMark({ name }: { name: string }) {\n  return (\n    <span\n      className=\"flex size-6 items-center justify-center rounded-md bg-white/20 text-[11px] font-bold tracking-tight text-white backdrop-blur-sm\"\n      aria-hidden\n    >\n      {name.slice(0, 1)}\n    </span>\n  );\n}\n\nfunction ProgressiveBottomBlur() {\n  return (\n    <div\n      className=\"pointer-events-none absolute inset-x-0 bottom-0 h-[55%]\"\n      aria-hidden\n    >\n      <div\n        className=\"absolute inset-0 backdrop-blur-[2px]\"\n        style={{\n          maskImage:\n            \"linear-gradient(to top, black 0%, black 35%, transparent 100%)\",\n          WebkitMaskImage:\n            \"linear-gradient(to top, black 0%, black 35%, transparent 100%)\",\n        }}\n      />\n      <div\n        className=\"absolute inset-0 backdrop-blur-[6px]\"\n        style={{\n          maskImage:\n            \"linear-gradient(to top, black 0%, black 28%, transparent 72%)\",\n          WebkitMaskImage:\n            \"linear-gradient(to top, black 0%, black 28%, transparent 72%)\",\n        }}\n      />\n      <div\n        className=\"absolute inset-0 backdrop-blur-[14px]\"\n        style={{\n          maskImage:\n            \"linear-gradient(to top, black 0%, black 18%, transparent 55%)\",\n          WebkitMaskImage:\n            \"linear-gradient(to top, black 0%, black 18%, transparent 55%)\",\n        }}\n      />\n      <div\n        className=\"absolute inset-0 backdrop-blur-[28px]\"\n        style={{\n          maskImage:\n            \"linear-gradient(to top, black 0%, black 10%, transparent 40%)\",\n          WebkitMaskImage:\n            \"linear-gradient(to top, black 0%, black 10%, transparent 40%)\",\n        }}\n      />\n      <div className=\"absolute inset-0 bg-gradient-to-t from-black/45 via-black/10 to-transparent\" />\n    </div>\n  );\n}\n\nfunction StoryCard({ story }: { story: CustomerStory }) {\n  return (\n    <article\n      className={cn(\n        \"group relative aspect-square w-[min(78vw,22rem)] shrink-0 snap-start overflow-hidden rounded-2xl\",\n        \"sm:w-[min(42vw,24rem)] lg:w-[26rem]\",\n      )}\n    >\n      <Image\n        src={story.imageSrc}\n        alt={story.imageAlt}\n        fill\n        draggable={false}\n        sizes=\"(max-width: 640px) 78vw, (max-width: 1024px) 42vw, 416px\"\n        className=\"pointer-events-none object-cover transition duration-500 group-hover:scale-[1.03]\"\n        priority={story.id === \"velocity\"}\n      />\n      <div\n        className=\"pointer-events-none absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent\"\n        aria-hidden\n      />\n      <ProgressiveBottomBlur />\n      <div\n        className=\"pointer-events-none absolute inset-0 bg-[#c8c8c8]/0 transition duration-300 group-hover:bg-[#c8c8c8]/25\"\n        aria-hidden\n      />\n\n      <div className=\"absolute inset-0 flex flex-col justify-between p-5 sm:p-6\">\n        <div className=\"flex items-center gap-2.5\">\n          <BrandMark name={story.brand} />\n          <span className=\"text-[15px] font-semibold tracking-tight text-white\">\n            {story.brand}\n          </span>\n        </div>\n\n        <div className=\"flex items-end justify-between gap-3\">\n          <ul className=\"flex min-w-0 flex-wrap gap-1.5\">\n            {story.tags.map((tag) => (\n              <li\n                key={tag}\n                className=\"rounded-full bg-black/35 px-2.5 py-1 text-[11px] font-medium text-white/95 backdrop-blur-md\"\n              >\n                {tag}\n              </li>\n            ))}\n          </ul>\n          <button\n            type=\"button\"\n            aria-label={`Open ${story.brand} story`}\n            className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-white/15 text-white backdrop-blur-md transition hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70\"\n          >\n            <PlusIcon className=\"size-5\" strokeWidth={2} />\n          </button>\n        </div>\n      </div>\n    </article>\n  );\n}\n\nexport type CustomerStoriesCarouselProps = {\n  stories?: CustomerStory[];\n  viewAllHref?: string;\n  viewAllLabel?: string;\n  className?: string;\n};\n\nconst DRAG_THRESHOLD = 6;\nconst FRICTION = 0.92;\nconst MIN_VELOCITY = 0.12;\n\nexport function CustomerStoriesCarousel({\n  stories = STORIES,\n  viewAllHref = \"#\",\n  viewAllLabel = \"View All Of Our Customer Stories\",\n  className,\n}: CustomerStoriesCarouselProps) {\n  const scrollerRef = useRef<HTMLDivElement>(null);\n  const [canPrev, setCanPrev] = useState(false);\n  const [canNext, setCanNext] = useState(true);\n\n  const dragRef = useRef<{\n    pointerId: number;\n    startX: number;\n    startScroll: number;\n    lastX: number;\n    lastT: number;\n    velocity: number;\n    moved: boolean;\n  } | null>(null);\n  const inertiaRef = useRef<number | null>(null);\n  const suppressClickRef = useRef(false);\n  const wheelSnapTimerRef = useRef<number | null>(null);\n  const navRafRef = useRef<number | null>(null);\n  const interactingRef = useRef(false);\n\n  const stopInertia = useCallback(() => {\n    if (inertiaRef.current != null) {\n      cancelAnimationFrame(inertiaRef.current);\n      inertiaRef.current = null;\n    }\n  }, []);\n\n  const setSnapEnabled = useCallback((enabled: boolean) => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    // Inline style so React className re-renders can't re-enable snap mid-drag\n    el.style.scrollSnapType = enabled ? \"\" : \"none\";\n  }, []);\n\n  const syncNav = useCallback(() => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    const max = el.scrollWidth - el.clientWidth;\n    const nextPrev = el.scrollLeft > 4;\n    const nextNext = el.scrollLeft < max - 4;\n    setCanPrev((prev) => (prev === nextPrev ? prev : nextPrev));\n    setCanNext((prev) => (prev === nextNext ? prev : nextNext));\n  }, []);\n\n  const scheduleSyncNav = useCallback(() => {\n    if (navRafRef.current != null) return;\n    navRafRef.current = requestAnimationFrame(() => {\n      navRafRef.current = null;\n      syncNav();\n    });\n  }, [syncNav]);\n\n  const clampScroll = useCallback((left: number) => {\n    const el = scrollerRef.current;\n    if (!el) return left;\n    const max = Math.max(0, el.scrollWidth - el.clientWidth);\n    return Math.min(max, Math.max(0, left));\n  }, []);\n\n  const finishInteraction = useCallback(() => {\n    interactingRef.current = false;\n    setSnapEnabled(true);\n    const el = scrollerRef.current;\n    if (el) el.style.cursor = \"\";\n    syncNav();\n  }, [setSnapEnabled, syncNav]);\n\n  const startInertia = useCallback(\n    (initialVelocity: number) => {\n      stopInertia();\n      let velocity = initialVelocity;\n      const tick = () => {\n        const el = scrollerRef.current;\n        if (!el || Math.abs(velocity) < MIN_VELOCITY) {\n          inertiaRef.current = null;\n          finishInteraction();\n          return;\n        }\n        const next = clampScroll(el.scrollLeft + velocity);\n        if (next === el.scrollLeft) {\n          inertiaRef.current = null;\n          finishInteraction();\n          return;\n        }\n        el.scrollLeft = next;\n        velocity *= FRICTION;\n        scheduleSyncNav();\n        inertiaRef.current = requestAnimationFrame(tick);\n      };\n      inertiaRef.current = requestAnimationFrame(tick);\n    },\n    [clampScroll, finishInteraction, scheduleSyncNav, stopInertia],\n  );\n\n  useEffect(() => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    syncNav();\n    const onScroll = () => {\n      if (interactingRef.current) return;\n      scheduleSyncNav();\n    };\n    el.addEventListener(\"scroll\", onScroll, { passive: true });\n    const ro = new ResizeObserver(syncNav);\n    ro.observe(el);\n    return () => {\n      el.removeEventListener(\"scroll\", onScroll);\n      ro.disconnect();\n      stopInertia();\n      if (navRafRef.current != null) cancelAnimationFrame(navRafRef.current);\n    };\n  }, [scheduleSyncNav, stopInertia, syncNav, stories]);\n\n  useEffect(() => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    const onWheel = (e: WheelEvent) => {\n      if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;\n      if (el.scrollWidth <= el.clientWidth) return;\n      e.preventDefault();\n      stopInertia();\n      interactingRef.current = true;\n      setSnapEnabled(false);\n      el.scrollLeft = clampScroll(el.scrollLeft + e.deltaY);\n      scheduleSyncNav();\n      if (wheelSnapTimerRef.current != null) {\n        window.clearTimeout(wheelSnapTimerRef.current);\n      }\n      wheelSnapTimerRef.current = window.setTimeout(() => {\n        wheelSnapTimerRef.current = null;\n        finishInteraction();\n      }, 160);\n    };\n    el.addEventListener(\"wheel\", onWheel, { passive: false });\n    return () => {\n      el.removeEventListener(\"wheel\", onWheel);\n      if (wheelSnapTimerRef.current != null) {\n        window.clearTimeout(wheelSnapTimerRef.current);\n      }\n    };\n  }, [\n    clampScroll,\n    finishInteraction,\n    scheduleSyncNav,\n    setSnapEnabled,\n    stopInertia,\n  ]);\n\n  const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n    if (e.button !== 0) return;\n    const target = e.target as HTMLElement;\n    if (target.closest(\"button, a\")) return;\n\n    const el = scrollerRef.current;\n    if (!el) return;\n\n    stopInertia();\n    if (wheelSnapTimerRef.current != null) {\n      window.clearTimeout(wheelSnapTimerRef.current);\n      wheelSnapTimerRef.current = null;\n    }\n\n    interactingRef.current = true;\n    setSnapEnabled(false);\n    dragRef.current = {\n      pointerId: e.pointerId,\n      startX: e.clientX,\n      startScroll: el.scrollLeft,\n      lastX: e.clientX,\n      lastT: performance.now(),\n      velocity: 0,\n      moved: false,\n    };\n    el.setPointerCapture(e.pointerId);\n  };\n\n  const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n    const drag = dragRef.current;\n    const el = scrollerRef.current;\n    if (!drag || !el || drag.pointerId !== e.pointerId) return;\n\n    const dx = e.clientX - drag.startX;\n    if (!drag.moved && Math.abs(dx) < DRAG_THRESHOLD) return;\n\n    if (!drag.moved) {\n      drag.moved = true;\n      suppressClickRef.current = true;\n      el.style.cursor = \"grabbing\";\n    }\n\n    e.preventDefault();\n\n    const now = performance.now();\n    const dt = Math.max(8, now - drag.lastT);\n    const frameDx = e.clientX - drag.lastX;\n    const instant = (-frameDx / dt) * 16;\n    drag.velocity = drag.velocity * 0.65 + instant * 0.35;\n    drag.lastX = e.clientX;\n    drag.lastT = now;\n\n    el.scrollLeft = clampScroll(drag.startScroll - dx);\n    scheduleSyncNav();\n  };\n\n  const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {\n    const drag = dragRef.current;\n    const el = scrollerRef.current;\n    if (!drag || drag.pointerId !== e.pointerId) return;\n\n    dragRef.current = null;\n\n    if (el?.hasPointerCapture(e.pointerId)) {\n      el.releasePointerCapture(e.pointerId);\n    }\n\n    if (drag.moved) {\n      const boost = Math.max(-48, Math.min(48, drag.velocity * 18));\n      if (Math.abs(boost) > MIN_VELOCITY * 4) {\n        startInertia(boost);\n      } else {\n        finishInteraction();\n      }\n      window.setTimeout(() => {\n        suppressClickRef.current = false;\n      }, 0);\n    } else {\n      finishInteraction();\n    }\n  };\n\n  const onClickCapture = (e: ReactMouseEvent) => {\n    if (!suppressClickRef.current) return;\n    e.preventDefault();\n    e.stopPropagation();\n    suppressClickRef.current = false;\n  };\n\n  const scrollByCard = (dir: -1 | 1) => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    stopInertia();\n    interactingRef.current = false;\n    setSnapEnabled(true);\n    el.style.cursor = \"\";\n    const card = el.querySelector<HTMLElement>(\"[data-story-card]\");\n    const step = card ? card.offsetWidth + 16 : el.clientWidth * 0.7;\n    el.scrollBy({ left: dir * step, behavior: \"smooth\" });\n  };\n\n  return (\n    <section className={cn(\"w-full\", className)} aria-label=\"Customer stories\">\n      <div\n        ref={scrollerRef}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        onClickCapture={onClickCapture}\n        className={cn(\n          \"flex cursor-grab touch-pan-y gap-4 overflow-x-auto pb-1 select-none\",\n          \"snap-x snap-mandatory\",\n          \"[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        )}\n      >\n        {stories.map((story) => (\n          <div key={story.id} data-story-card className=\"shrink-0\">\n            <StoryCard story={story} />\n          </div>\n        ))}\n      </div>\n\n      <div className=\"mt-8 flex items-center justify-between gap-4\">\n        <a\n          href={viewAllHref}\n          className=\"inline-flex items-center gap-1.5 text-sm font-medium text-zinc-800 transition hover:text-zinc-950 dark:text-zinc-200 dark:hover:text-white\"\n        >\n          {viewAllLabel}\n          <ArrowRightIcon className=\"size-4\" aria-hidden />\n        </a>\n\n        <div className=\"flex items-center gap-2.5\">\n          <button\n            type=\"button\"\n            aria-label=\"Previous stories\"\n            disabled={!canPrev}\n            onClick={() => scrollByCard(-1)}\n            className={cn(\n              \"flex size-11 items-center justify-center rounded-full transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400\",\n              canPrev\n                ? \"bg-zinc-800 text-white hover:bg-zinc-700 dark:bg-zinc-200 dark:text-zinc-950 dark:hover:bg-white\"\n                : \"cursor-not-allowed bg-zinc-200 text-zinc-400 dark:bg-zinc-800 dark:text-zinc-600\",\n            )}\n          >\n            <ArrowLeftIcon className=\"size-5\" strokeWidth={1.75} />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Next stories\"\n            disabled={!canNext}\n            onClick={() => scrollByCard(1)}\n            className={cn(\n              \"flex size-11 items-center justify-center rounded-full transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400\",\n              canNext\n                ? \"bg-zinc-800 text-white hover:bg-zinc-700 dark:bg-zinc-200 dark:text-zinc-950 dark:hover:bg-white\"\n                : \"cursor-not-allowed bg-zinc-200 text-zinc-400 dark:bg-zinc-800 dark:text-zinc-600\",\n            )}\n          >\n            <ArrowRightIcon className=\"size-5\" strokeWidth={1.75} />\n          </button>\n        </div>\n      </div>\n    </section>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}