{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "breathe-hero",
  "title": "Breathe Arch Stagger Hero",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 16 hero variants: https://avaraui.com/view/hero. This is the Breathe hero. Skip the question if the user already named one. An ethereal hero section featuring an elegant serif headline and a staggered 5-card curved arch. On scroll, the center card scales up to 1.4x while surrounding cards gently drift outward and dim, creating a breathtaking focal zoom.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/BreatheHeroBlock.tsx",
      "content": "\"use client\";\n\nimport React, { forwardRef, useEffect, useRef } from \"react\";\nimport { ArrowRight, ArrowUpRight, Check, Sparkles, Star } from \"lucide-react\";\nimport Button from \"./Button\";\nimport { cn } from \"@/lib/utils\";\nimport { accentTokens } from \"@/lib/accent\";\nimport { usePrefersReducedMotion } from \"@/lib/animate\";\n\n/* ═══════════════════════════════════════════════\n   TYPES & INTERFACES\n   ═══════════════════════════════════════════════ */\n\nexport interface BreatheCardItem {\n  id?: string | number;\n  image?: string;\n  caption?: string;\n  color?: string;\n  alt?: string;\n  href?: string;\n}\n\nexport type BreatheScrollMode = \"grow\" | \"parallax\" | \"spread\" | \"none\";\nexport type BreatheIntroMode = \"fan\" | \"rise\" | \"scale\" | \"none\";\nexport type BreatheHoverMode = \"zoom\" | \"lift\" | \"both\" | \"none\";\nexport type BreatheTextIntro = \"blur\" | \"rise\" | \"fade\" | \"none\";\n\nexport interface BreatheHeroProps\n  extends React.HTMLAttributes<HTMLElement> {\n  title?: string;\n  titleAccent?: string;\n  italicAccent?: boolean;\n  titleStyle?: \"serif\" | \"sans\" | \"gradient\";\n  description?: string;\n  badge?: React.ReactNode;\n  badgeText?: string;\n  badgeHref?: string;\n  badgeHighlight?: string;\n  buttonLabel?: string;\n  buttonHref?: string;\n  buttonVariant?: \"filled\" | \"outline\" | \"soft\";\n  buttonArrow?: boolean;\n  secondaryButtonLabel?: string;\n  secondaryButtonHref?: string;\n  secondaryButtonIcon?: React.ReactNode;\n  showTrust?: boolean;\n  trustRating?: string | number;\n  trustCount?: string;\n  trustAvatars?: string[];\n  features?: string[];\n  cards?: BreatheCardItem[];\n  images?: string[];\n  captions?: \"hover\" | \"always\" | \"off\";\n  scrollMode?: BreatheScrollMode;\n  introMode?: BreatheIntroMode;\n  hoverMode?: BreatheHoverMode;\n  textIntro?: BreatheTextIntro;\n  scrollScale?: number;\n  stagger?: number;\n  cardHeight?: number;\n  cardRadius?: number | string;\n  gap?: number;\n  accent?: string;\n  color?: string;\n  internalScroll?: boolean;\n  containerHeight?: number | string;\n  scrollDistance?: number | string;\n  linkComponent?: React.ElementType;\n  imageComponent?: React.ElementType;\n}\n\n/* ═══════════════════════════════════════════════\n   DEFAULT PRESETS & BREATHE COLOR CARDS\n   ═══════════════════════════════════════════════ */\n\nexport const DEFAULT_BREATHE_CARDS: BreatheCardItem[] = [\n  {\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n    caption: \"Evergreen Canopy\",\n    color: \"#DCE4DA\",\n    alt: \"Sunlit morning mist in the pine grove\",\n  },\n  {\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n    caption: \"Misty Mountain Ridge\",\n    color: \"#C1CCC4\",\n    alt: \"Low-hanging cloud bank at dawn\",\n  },\n  {\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1000,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n    caption: \"Yosemite Sanctuary\",\n    color: \"#D5C9BD\",\n    alt: \"Glacial valley and mirror lake reflections\",\n  },\n  {\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n    caption: \"Redwood Sunbeam Trail\",\n    color: \"#C1CCC4\",\n    alt: \"Ancient forest canopy illuminated by morning light\",\n  },\n  {\n    image:\n      \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1000,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n    caption: \"Emerald Alpine Lake\",\n    color: \"#DCE4DA\",\n    alt: \"Crystal turquoise meltwater enclosed by ridges\",\n  },\n];\n\nexport const DEFAULT_TRUST_AVATARS: string[] = [\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_120,c_limit/v1790145139/pexels-photo-1036623-1790145143487.jpg\",\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_120,c_limit/v1790145142/pexels-photo-2043590-1790145143488.jpg\",\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_120,c_limit/v1790145139/pexels-photo-1587009-1790145143488.jpg\",\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_120,c_limit/v1790082180/pexels-photo-1520760-1790082180553.jpg\",\n];\n\nexport const DEFAULT_BREATHE_FEATURES: string[] = [\n  \"100% Confidential\",\n  \"In-person & Online\",\n  \"Licensed Specialists\",\n];\n\nconst PARALLAX_FACTORS = [0.35, 0.9, 0.15, 0.7, 0.5];\nconst EASING_CUBIC = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Scroll animations run across this much of the scroll progress. */\nconst SCROLL_END = 0.85;\n\n/** The spring smoothing scroll progress into card movement. */\nconst SCROLL_SPRING = { stiffness: 90, damping: 26, mass: 0.5 };\n\n/**\n * Where a card sits at a given scroll progress.\n *\n * - \"grow\" (Exact 1:1 Hero Breathe Framer effect): all 5 cards share the same\n *   base height and sit at stepped Y offsets (baseY = absOffset * stagger): Card\n *   3 at y=0, Cards 2&4 at y=28px, Cards 1&5 at y=56px. On scroll, ONLY the Center\n *   Card scales up (1 -> scrollScale, e.g. 1.28x), while Cards 1&2 shift left\n *   together and Cards 4&5 shift right together to clear its expanded width,\n *   keeping the inter-card gap tight and uniform. ZERO rotation at all times!\n * - \"parallax\": cards glide vertically at differentiated speeds.\n * - \"spread\": cards fan out horizontally.\n */\nfunction cardTransform(\n  index: number,\n  offset: number,\n  progress: number,\n  scrollMode: BreatheScrollMode,\n  scrollScale: number,\n  stagger: number,\n): string {\n  const absOffset = Math.abs(offset);\n  const signOffset = Math.sign(offset);\n  const baseY = absOffset * stagger;\n  const t = Math.min(Math.max(progress / SCROLL_END, 0), 1);\n  const mix = (from: number, to: number) => from + (to - from) * t;\n\n  switch (scrollMode) {\n    case \"grow\": {\n      const targetX = offset === 0 ? 0 : signOffset * (absOffset === 1 ? 18 : 22);\n      return `translate3d(${mix(0, targetX)}%, ${baseY}px, 0) scale(${mix(1, offset === 0 ? scrollScale : 1)})`;\n    }\n    case \"parallax\": {\n      const factor = PARALLAX_FACTORS[index % PARALLAX_FACTORS.length];\n      return `translate3d(0, ${mix(baseY, baseY - (60 + factor * 170))}px, 0)`;\n    }\n    case \"spread\":\n      return `translate3d(${mix(0, signOffset * absOffset * 12)}%, ${mix(baseY, baseY + absOffset * 10)}px, 0)`;\n    default:\n      return `translateY(${baseY}px)`;\n  }\n}\n\n/** An entrance played once by CSS, so text never waits on JavaScript to appear. */\nfunction introStyle(\n  enabled: boolean,\n  { delay, duration, y, blur = 0, scale = 1 }: { delay: number; duration: number; y: number; blur?: number; scale?: number },\n): React.CSSProperties | undefined {\n  if (!enabled) return undefined;\n  return {\n    \"--intro-y\": `${y}px`,\n    \"--intro-blur\": `${blur}px`,\n    \"--intro-scale\": scale,\n    animation: `breathe-intro ${duration}s ${EASING_CUBIC} ${delay}s backwards`,\n  } as React.CSSProperties;\n}\n\nconst BREATHE_MOTION_CSS = `\n@keyframes breathe-intro {\n  from { opacity: 0; transform: translateY(var(--intro-y, 0px)) scale(var(--intro-scale, 1)); filter: blur(var(--intro-blur, 0px)); }\n}\n@keyframes breathe-card-in {\n  from { transform: translate(var(--intro-x, 0px), var(--intro-y, 0px)) scale(var(--intro-scale, 1)); }\n}\n@keyframes breathe-fade-in { from { opacity: 0; } }\n@keyframes breathe-underline {\n  0% { stroke-dasharray: 0 1; stroke-dashoffset: 0; opacity: 0.45; }\n  45%, 70% { stroke-dasharray: 1 1; stroke-dashoffset: 0; opacity: 1; }\n  100% { stroke-dasharray: 0 1; stroke-dashoffset: -1; opacity: 0.45; }\n}\n.breathe-underline { animation: breathe-underline 3.2s ease-in-out infinite; }\n@media (prefers-reduced-motion: reduce) {\n  [data-breathe-intro] { animation: none !important; }\n  .breathe-underline { animation: none; stroke-dasharray: 1 1; opacity: 0.7; }\n}\n`;\n\n/* ═══════════════════════════════════════════════\n   CARD ARCH ITEM COMPONENT\n   ═══════════════════════════════════════════════ */\n\ninterface BreatheCardProps {\n  card: BreatheCardItem;\n  index: number;\n  offset: number;\n  /** Receives the element the scroll animation moves. */\n  cardRef: (node: HTMLDivElement | null) => void;\n  scrollMode: BreatheScrollMode;\n  introMode: BreatheIntroMode;\n  hoverMode: BreatheHoverMode;\n  showCaption: boolean;\n  captionsMode: \"hover\" | \"always\" | \"off\";\n  scrollScale: number;\n  stagger: number;\n  cardHeight?: number;\n  cardRadius: number | string;\n  imageComponent?: React.ElementType;\n  linkComponent?: React.ElementType;\n  prefersReducedMotion: boolean;\n}\n\nfunction BreatheCard({\n  card,\n  index,\n  offset,\n  cardRef,\n  scrollMode,\n  introMode,\n  hoverMode,\n  showCaption,\n  captionsMode,\n  scrollScale,\n  stagger,\n  cardHeight,\n  cardRadius,\n  imageComponent: ImageComp,\n  linkComponent: LinkComp,\n  prefersReducedMotion,\n}: BreatheCardProps) {\n  const isCenter = offset === 0;\n  const absOffset = Math.abs(offset);\n  const isOuter = absOffset >= 2;\n\n  // Entrance: completely upright (rotate: 0). The keyframes read these values.\n  const entrance: React.CSSProperties | undefined =\n    introMode === \"fan\"\n      ? ({ \"--intro-x\": `${-offset * 25}%`, \"--intro-y\": \"30px\", \"--intro-scale\": 0.97 } as React.CSSProperties)\n      : introMode === \"rise\"\n        ? ({ \"--intro-y\": \"50px\" } as React.CSSProperties)\n        : introMode === \"scale\"\n          ? ({ \"--intro-y\": \"20px\", \"--intro-scale\": 0.92 } as React.CSSProperties)\n          : undefined;\n\n  const delay = (introMode === \"fan\" ? 0.35 : 0.2) + absOffset * 0.08;\n\n  const Comp = card.href ? LinkComp || \"a\" : \"div\";\n  const linkProps = card.href ? { href: card.href } : {};\n\n  // All 5 cards share the exact same base height; the stepped arch comes from baseY = absOffset * stagger\n  const cardHeightClass =\n    \"h-[185px] sm:h-[240px] md:h-[285px] lg:h-[315px]\";\n\n  return (\n    <div\n      data-breathe-intro={entrance ? \"\" : undefined}\n      className={cn(\n        \"relative transition-all will-change-transform flex items-start justify-center shrink-0\",\n        isOuter ? \"hidden md:flex\" : \"flex\"\n      )}\n      style={{\n        flex: isCenter ? 1.18 : 1,\n        minWidth: 0,\n        maxWidth: isCenter ? 250 : 210,\n        zIndex: 10 - absOffset,\n        transformOrigin: \"50% 42%\",\n        ...entrance,\n        animation: entrance\n          ? `breathe-card-in 1.2s ${EASING_CUBIC} ${delay}s backwards, breathe-fade-in 0.6s ease-out ${delay}s backwards`\n          : undefined,\n      }}\n    >\n      {/* Outer hover wrapper for lift & zoom interactions */}\n      <Comp\n        {...linkProps}\n        className={cn(\n          \"group/card block relative w-full cursor-pointer select-none transition-transform duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n          (hoverMode === \"lift\" || hoverMode === \"both\") &&\n            \"hover:-translate-y-2.5\"\n        )}\n      >\n        <div\n          ref={cardRef}\n          style={{\n            transform: cardTransform(\n              index,\n              offset,\n              0,\n              prefersReducedMotion ? \"none\" : scrollMode,\n              scrollScale,\n              stagger,\n            ),\n            borderRadius: cardRadius,\n            transformOrigin: \"50% 42%\",\n            ...(cardHeight ? { height: cardHeight } : {}),\n          }}\n          className={cn(\n            \"relative w-full overflow-hidden border border-border/30 bg-muted/40 shadow-none\",\n            !cardHeight && cardHeightClass\n          )}\n        >\n          {/* Card Image with Slow Smooth Zoom on Hover */}\n          {card.image ? (\n            ImageComp ? (\n              <ImageComp\n                src={card.image}\n                alt={card.alt || card.caption || \"Hero card\"}\n                className={cn(\n                  \"size-full object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n                  (hoverMode === \"zoom\" || hoverMode === \"both\") &&\n                    \"group-hover/card:scale-105\"\n                )}\n              />\n            ) : (\n              <img\n                src={card.image}\n                alt={card.alt || card.caption || \"Hero card\"}\n                loading=\"lazy\"\n                className={cn(\n                  \"size-full object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n                  (hoverMode === \"zoom\" || hoverMode === \"both\") &&\n                    \"group-hover/card:scale-105\"\n                )}\n              />\n            )\n          ) : (\n            <div\n              style={{ backgroundColor: card.color || \"#C9D3C4\" }}\n              className=\"grid size-full place-items-center text-xs sm:text-sm md:text-base font-medium text-foreground/70 select-none\"\n            >\n              {card.alt || `Image ${index + 1}`}\n            </div>\n          )}\n\n          {/* Subtle Bottom Scrim for Caption Legibility */}\n          {card.image && showCaption && card.caption && (\n            <div className=\"pointer-events-none absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/50 to-transparent opacity-0 transition-opacity duration-300 group-hover/card:opacity-100\" />\n          )}\n\n          {/* Floating Frosted Glass Caption Pill */}\n          {showCaption && card.caption && (\n            <span\n              className={cn(\n                \"absolute bottom-2 sm:bottom-3 left-2 sm:left-3 z-10 max-w-[calc(100%-16px)] sm:max-w-[calc(100%-24px)] truncate rounded-full border border-white/20 bg-background/85 px-2 sm:px-3 py-1 sm:py-1.5 text-[10px] sm:text-xs font-medium text-foreground backdrop-blur-md transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n                captionsMode === \"always\"\n                  ? \"opacity-100 translate-y-0\"\n                  : \"opacity-0 translate-y-2 group-hover/card:opacity-100 group-hover/card:translate-y-0\"\n              )}\n            >\n              {card.caption}\n            </span>\n          )}\n        </div>\n      </Comp>\n    </div>\n  );\n}\n\n/* ═══════════════════════════════════════════════\n   MAIN COMPONENT: BreatheHero\n   ═══════════════════════════════════════════════ */\n\nexport const BreatheHero = forwardRef<HTMLElement, BreatheHeroProps>(\n  (\n    {\n      title = \"Crafted for depth,\",\n      titleAccent = \"built for motion.\",\n      italicAccent = true,\n      titleStyle = \"gradient\",\n      description = \"Create immersive digital experiences where fluid scroll choreography, spatial architecture, and tactile visual polish move in effortless harmony.\",\n      badge,\n      badgeText = \"Interactive Motion Architecture\",\n      badgeHref = \"#explore\",\n      badgeHighlight = \"Studio v2.4\",\n      buttonLabel = \"Start Exploring\",\n      buttonHref = \"#get-started\",\n      buttonVariant = \"filled\",\n      buttonArrow = true,\n      secondaryButtonLabel = \"View Showroom\",\n      secondaryButtonHref = \"#showroom\",\n      secondaryButtonIcon,\n      showTrust = false,\n      trustRating = \"4.9/5\",\n      trustCount = \"from 12,000+ reviews\",\n      trustAvatars = DEFAULT_TRUST_AVATARS,\n      features = [],\n      cards,\n      images,\n      captions = \"hover\",\n      scrollMode = \"grow\",\n      introMode = \"fan\",\n      hoverMode = \"zoom\",\n      textIntro = \"blur\",\n      scrollScale = 1.26,\n      stagger = 28,\n      cardHeight,\n      cardRadius = \"var(--radius-2xl, 22px)\",\n      gap = 14,\n      accent,\n      color,\n      internalScroll = true,\n      containerHeight,\n      scrollDistance,\n      linkComponent: LinkComp,\n      imageComponent,\n      className,\n      style,\n      ...props\n    },\n    ref\n  ) => {\n    const containerRef = useRef<HTMLElement>(null);\n    const scrollContainerRef = useRef<HTMLDivElement>(null);\n    const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n    const prefersReducedMotion = usePrefersReducedMotion();\n    const textIntroOn = textIntro !== \"none\";\n\n    // Resolve 5 cards\n    const resolvedCards: BreatheCardItem[] = React.useMemo(() => {\n      if (cards && cards.length > 0) return cards;\n      if (images && images.length > 0) {\n        return Array.from({ length: 5 }, (_, i) => ({\n          image: images[i % images.length],\n          caption: DEFAULT_BREATHE_CARDS[i % DEFAULT_BREATHE_CARDS.length].caption,\n          color: DEFAULT_BREATHE_CARDS[i % DEFAULT_BREATHE_CARDS.length].color,\n          alt: DEFAULT_BREATHE_CARDS[i % DEFAULT_BREATHE_CARDS.length].alt,\n        }));\n      }\n      return DEFAULT_BREATHE_CARDS;\n    }, [cards, images]);\n\n    const centerIndex = Math.floor(resolvedCards.length / 2);\n    const Comp = LinkComp || \"a\";\n\n    // Scroll progress, smoothed by a spring and written straight to the cards. It\n    // works when scrolling inside the section, scrolling the page, and scrolling\n    // the page around an iframe preview. The loop runs only while cards are moving.\n    useEffect(() => {\n      if (prefersReducedMotion || scrollMode === \"none\") return;\n      const section = containerRef.current;\n      if (!section) return;\n      const scroller = internalScroll ? scrollContainerRef.current : null;\n\n      let parentWin: Window | null = null;\n      try {\n        if (window.parent !== window && window.parent.location.origin === window.location.origin) {\n          parentWin = window.parent;\n        }\n      } catch {\n        parentWin = null;\n      }\n\n      const clamp = (value: number) => Math.min(Math.max(value, 0), 1);\n      const readProgress = () => {\n        const rect = section.getBoundingClientRect();\n        const page = rect.height ? clamp(-rect.top / rect.height) : 0;\n        const inner = scroller\n          ? clamp(scroller.scrollTop / (scroller.scrollHeight - scroller.clientHeight || 1))\n          : 0;\n        let framed = 0;\n        const frame = parentWin ? (window.frameElement as HTMLElement | null) : null;\n        if (parentWin && frame) {\n          const frameRect = frame.getBoundingClientRect();\n          const start = (parentWin.innerHeight || 800) * 0.45;\n          const end = -frameRect.height * 0.35;\n          framed = clamp((start - frameRect.top) / (start - end));\n        }\n        return Math.max(page, inner, framed);\n      };\n\n      const paint = (progress: number) => {\n        cardRefs.current.forEach((node, index) => {\n          if (!node) return;\n          node.style.transform = cardTransform(index, index - centerIndex, progress, scrollMode, scrollScale, stagger);\n        });\n      };\n\n      let goal = readProgress();\n      let position = goal;\n      let velocity = 0;\n      let raf = 0;\n      let last = 0;\n      paint(position);\n\n      const tick = (now: number) => {\n        const dt = Math.min((now - last) / 1000, 1 / 40);\n        last = now;\n        const { stiffness, damping, mass } = SCROLL_SPRING;\n        velocity += ((stiffness * (goal - position) - damping * velocity) / mass) * dt;\n        position += velocity * dt;\n        if (Math.abs(goal - position) < 0.0005 && Math.abs(velocity) < 0.0005) {\n          position = goal;\n          velocity = 0;\n          raf = 0;\n          paint(position);\n          return;\n        }\n        paint(position);\n        raf = requestAnimationFrame(tick);\n      };\n\n      const wake = () => {\n        goal = readProgress();\n        if (raf || goal === position) return;\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      };\n\n      window.addEventListener(\"scroll\", wake, { passive: true, capture: true });\n      window.addEventListener(\"resize\", wake);\n      parentWin?.addEventListener(\"scroll\", wake, { passive: true });\n      return () => {\n        cancelAnimationFrame(raf);\n        window.removeEventListener(\"scroll\", wake, { capture: true });\n        window.removeEventListener(\"resize\", wake);\n        parentWin?.removeEventListener(\"scroll\", wake);\n      };\n    }, [prefersReducedMotion, scrollMode, scrollScale, stagger, internalScroll, centerIndex]);\n\n    const customStyle: React.CSSProperties = {\n      ...accentTokens(accent || color),\n      ...style,\n    };\n\n    const content = (\n      <div className=\"container relative z-10 mx-auto px-4 sm:px-6 lg:px-8\">\n        <div className=\"flex flex-col items-center text-center\">\n          {/* ═════════════════════════════════════════\n              1. REFINED GLASSMORPHIC ANNOUNCEMENT PILL BADGE\n              ═════════════════════════════════════════ */}\n          {badge ? (\n            <div\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 0.9, delay: 0.05, y: -10, blur: 6 })}\n              className=\"mb-4 sm:mb-6\"\n            >\n              {badge}\n            </div>\n          ) : badgeText ? (\n            <div\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 0.9, delay: 0.05, y: -10, blur: 6 })}\n              className=\"mb-4 sm:mb-6 max-w-full\"\n            >\n              <Comp\n                href={badgeHref}\n                className=\"group inline-flex items-center justify-center gap-1.5 sm:gap-2.5 max-w-[85vw] sm:max-w-full rounded-full border border-primary/20 bg-background/75 hover:bg-background/95 px-2.5 sm:px-4 py-1 sm:py-1.5 text-[11px] sm:text-xs font-medium text-foreground shadow-[0_2px_12px_-3px_rgba(0,0,0,0.06)] backdrop-blur-md transition-all duration-300 hover:border-primary/40\"\n              >\n                <Sparkles className=\"size-3 sm:size-3.5 text-primary transition-transform duration-300 group-hover:rotate-12 shrink-0\" />\n                {badgeHighlight && (\n                  <>\n                    <span className=\"hidden sm:inline-flex items-center font-semibold text-primary shrink-0\">\n                      {badgeHighlight}\n                    </span>\n                    <span className=\"hidden sm:block h-3 w-px bg-border/80 shrink-0\" />\n                  </>\n                )}\n                <span className=\"sm:hidden truncate text-foreground/90 font-medium\">\n                  {badgeText.split(\" \").slice(-2).join(\" \")}\n                </span>\n                <span className=\"hidden sm:inline truncate text-muted-foreground group-hover:text-foreground transition-colors\">\n                  {badgeText}\n                </span>\n                <ArrowUpRight className=\"size-3 sm:size-3.5 text-primary/80 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 shrink-0\" />\n              </Comp>\n            </div>\n          ) : null}\n\n          {/* ═════════════════════════════════════════\n              2. COHESIVE MODERN DISPLAY HEADLINE (100% RESPONSIVE)\n              ═════════════════════════════════════════ */}\n          <h1\n            data-breathe-intro=\"\"\n            style={{\n              ...introStyle(textIntroOn, { duration: 1.1, delay: 0.12, y: 18, blur: 8 }),\n              fontFamily:\n                titleStyle === \"serif\"\n                  ? '\"Playfair Display\", \"Newsreader\", var(--font-cooper), Georgia, serif'\n                  : '\"Plus Jakarta Sans\", \"Inter\", var(--font-sans), system-ui, sans-serif',\n            }}\n            className=\"max-w-4xl px-1 sm:px-0 text-[1.75rem] sm:text-5xl md:text-6xl lg:text-[4.25rem] xl:text-[4.65rem] font-bold tracking-[-0.035em] text-foreground leading-[1.15] sm:leading-[1.08] text-balance\"\n          >\n            <span className=\"block text-foreground\">{title}</span>\n            {titleAccent && (\n              <span className=\"relative inline-block mt-0.5 sm:mt-1\">\n                <span\n                  className={cn(\n                    \"transition-colors duration-300\",\n                    titleStyle === \"gradient\"\n                      ? \"bg-gradient-to-r from-primary via-primary/85 to-primary/60 bg-clip-text text-transparent\"\n                      : \"text-primary\",\n                    titleStyle === \"serif\" && italicAccent && \"italic font-normal\"\n                  )}\n                >\n                  {titleAccent}\n                </span>\n                <svg\n                  viewBox=\"0 0 240 12\"\n                  fill=\"none\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  aria-hidden=\"true\"\n                  className=\"mx-auto mt-1 sm:mt-1.5 h-1.5 sm:h-2.5 w-32 sm:w-52 md:w-64 text-primary\"\n                >\n                  <path\n                    d=\"M3 9C62 2.5 178 2.5 237 9\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"3\"\n                    strokeLinecap=\"round\"\n                    className=\"opacity-15\"\n                  />\n                  <path\n                    d=\"M3 9C62 2.5 178 2.5 237 9\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"3\"\n                    strokeLinecap=\"round\"\n                    pathLength={1}\n                    className=\"breathe-underline\"\n                  />\n                </svg>\n              </span>\n            )}\n          </h1>\n\n          {/* ═════════════════════════════════════════\n              3. REFINED SUBTITLE DESCRIPTION\n              ═════════════════════════════════════════ */}\n          {description && (\n            <p\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 1.1, delay: 0.24, y: 14, blur: 6 })}\n              className=\"mt-3 sm:mt-5 max-w-xl sm:max-w-2xl px-2 sm:px-0 text-xs sm:text-base md:text-[1.0625rem] text-muted-foreground/90 leading-relaxed font-normal tracking-[-0.01em] text-balance\"\n            >\n              {description}\n            </p>\n          )}\n\n          {/* ═════════════════════════════════════════\n              4. CTA BUTTONS POWERED BY AVARA UI'S BUTTON COMPONENT\n              (FOLLOWS GLOBAL THEME --radius DYNAMICALLY)\n              ═════════════════════════════════════════ */}\n          {(buttonLabel || secondaryButtonLabel) && (\n            <div\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 1.1, delay: 0.36, y: 12, scale: 0.96 })}\n              className=\"mt-6 sm:mt-8 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 w-full sm:w-auto\"\n            >\n              {buttonLabel && (\n                <Comp href={buttonHref} className=\"w-full sm:w-auto inline-block cursor-pointer\">\n                  <Button\n                    variant=\"pricing-primary\"\n                    showArrow={buttonArrow}\n                    style={{ borderRadius: \"var(--radius)\" }}\n                    className=\"w-full sm:w-auto px-7 py-3.5 text-sm sm:text-base font-medium shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/35 cursor-pointer justify-center transition-all duration-200\"\n                  >\n                    {buttonLabel}\n                  </Button>\n                </Comp>\n              )}\n\n              {secondaryButtonLabel && (\n                <Comp href={secondaryButtonHref} className=\"w-full sm:w-auto inline-block cursor-pointer\">\n                  <Button\n                    variant=\"pricing-outline\"\n                    iconRight={\n                      secondaryButtonIcon || (\n                        <ArrowRight className=\"size-4 transition-transform duration-200 group-hover:translate-x-0.5 text-primary\" />\n                      )\n                    }\n                    style={{ borderRadius: \"var(--radius)\" }}\n                    className=\"w-full sm:w-auto px-6 py-3.5 text-sm sm:text-base font-medium border-border/70 hover:border-primary/50 cursor-pointer justify-center transition-all duration-200\"\n                  >\n                    {secondaryButtonLabel}\n                  </Button>\n                </Comp>\n              )}\n            </div>\n          )}\n\n          {/* ═════════════════════════════════════════\n              5. OPTIONAL TRUST RATING & CLIENT AVATARS\n              ═════════════════════════════════════════ */}\n          {showTrust && (\n            <div\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 1.1, delay: 0.46, y: 10, blur: 4 })}\n              className=\"mt-6 sm:mt-7 flex flex-wrap items-center justify-center gap-2.5 sm:gap-3 text-xs sm:text-sm text-muted-foreground\"\n            >\n              {trustAvatars && trustAvatars.length > 0 && (\n                <div className=\"flex -space-x-2.5 overflow-hidden\">\n                  {trustAvatars.map((src, i) => (\n                    <img\n                      key={i}\n                      src={src}\n                      alt=\"Client avatar\"\n                      className=\"inline-block size-7 sm:size-8 rounded-full ring-2 ring-background object-cover\"\n                      loading=\"lazy\"\n                    />\n                  ))}\n                </div>\n              )}\n              <div className=\"flex items-center gap-1.5\">\n                <div className=\"flex items-center text-amber-500\">\n                  {[...Array(5)].map((_, i) => (\n                    <Star\n                      key={i}\n                      className=\"size-3.5 sm:size-4 fill-amber-500 text-amber-500\"\n                    />\n                  ))}\n                </div>\n                {trustRating && (\n                  <span className=\"font-semibold text-foreground\">\n                    {trustRating}\n                  </span>\n                )}\n                {trustCount && (\n                  <span className=\"text-muted-foreground\">({trustCount})</span>\n                )}\n              </div>\n            </div>\n          )}\n\n          {/* ═════════════════════════════════════════\n              6. OPTIONAL FEATURE HIGHLIGHT CHIPS\n              ═════════════════════════════════════════ */}\n          {features && features.length > 0 && (\n            <div\n              data-breathe-intro=\"\"\n              style={introStyle(textIntroOn, { duration: 1, delay: 0.54, y: 8 })}\n              className=\"mt-5 flex flex-wrap items-center justify-center gap-2 sm:gap-3 text-xs font-medium text-muted-foreground\"\n            >\n              {features.map((feat, idx) => (\n                <div\n                  key={idx}\n                  className=\"inline-flex items-center gap-1.5 rounded-[var(--radius-full,9999px)] border border-border/60 bg-muted/40 px-3 py-1 backdrop-blur-xs\"\n                >\n                  <Check className=\"size-3 text-primary stroke-[2.5]\" />\n                  <span>{feat}</span>\n                </div>\n              ))}\n            </div>\n          )}\n\n          {/* ═════════════════════════════════════════\n              CARDS ROW: 5 CURVED ARCH MOTION CARDS\n              (STEPPED BASELINE ARCH, ZERO ROTATE, GENEROUS GAP BELOW BUTTONS)\n              ═════════════════════════════════════════ */}\n          <div\n            style={{\n              gap,\n            }}\n            className=\"relative mt-16 sm:mt-20 md:mt-24 lg:mt-28 flex w-full max-w-6xl mx-auto items-start justify-center px-2 sm:px-4 h-[260px] sm:h-[320px] md:h-[370px] lg:h-[400px]\"\n          >\n            {resolvedCards.map((card, index) => (\n              <BreatheCard\n                key={card.id ?? index}\n                card={card}\n                index={index}\n                offset={index - centerIndex}\n                cardRef={(node) => {\n                  cardRefs.current[index] = node;\n                }}\n                scrollMode={scrollMode}\n                introMode={introMode}\n                hoverMode={hoverMode}\n                showCaption={captions !== \"off\"}\n                captionsMode={captions}\n                scrollScale={scrollScale}\n                stagger={stagger}\n                cardHeight={cardHeight}\n                cardRadius={cardRadius}\n                imageComponent={imageComponent}\n                linkComponent={LinkComp}\n                prefersReducedMotion={prefersReducedMotion}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n    );\n\n    return (\n      <section\n        ref={(node) => {\n          (containerRef as any).current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) (ref as any).current = node;\n        }}\n        style={customStyle}\n        className={cn(\n          \"relative w-full h-full overflow-hidden bg-background text-foreground transition-colors duration-300\",\n          internalScroll ? \"p-0\" : \"pt-20 pb-14 sm:pt-28 sm:pb-20 lg:pt-36 lg:pb-28\",\n          className\n        )}\n        {...props}\n      >\n        {/* Display & Editorial Web Fonts */}\n        <style>{`\n          @import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400;1,500;1,600&family=Plus+Jakarta+Sans:wght@500;600;700&display=swap');\n          ${BREATHE_MOTION_CSS}\n        `}</style>\n\n        {internalScroll ? (\n          <div\n            ref={scrollContainerRef}\n            style={\n              containerHeight\n                ? {\n                    height:\n                      typeof containerHeight === \"number\"\n                        ? `${containerHeight}px`\n                        : containerHeight,\n                  }\n                : undefined\n            }\n            className=\"relative w-full h-full overflow-y-auto overflow-x-hidden [scrollbar-width:auto] [scrollbar-color:hsl(var(--muted-foreground)/0.4)_transparent] [&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted-foreground/35 hover:[&::-webkit-scrollbar-thumb]:bg-muted-foreground/55 [&::-webkit-scrollbar-track]:bg-transparent\"\n          >\n            <div\n              style={\n                scrollDistance\n                  ? {\n                      minHeight:\n                        typeof scrollDistance === \"number\"\n                          ? `${scrollDistance}px`\n                          : scrollDistance,\n                    }\n                  : undefined\n              }\n              className=\"relative w-full min-h-[138%] flex flex-col justify-start pt-20 sm:pt-28 md:pt-32 lg:pt-36 pb-20 sm:pb-28 lg:pb-36\"\n            >\n              {/* Ambient Top Subtle Background Lighting & Radial Glow */}\n              <div className=\"pointer-events-none absolute -top-28 left-1/2 -translate-x-1/2 size-[640px] rounded-full bg-primary/10 blur-[130px] opacity-75\" />\n              <div className=\"pointer-events-none absolute top-0 inset-x-0 h-96 bg-gradient-to-b from-primary/[0.06] via-primary/[0.02] to-transparent\" />\n              <div className=\"pointer-events-none absolute top-12 left-1/2 -translate-x-1/2 w-full max-w-5xl h-64 bg-[radial-gradient(ellipse_at_top,var(--primary)/0.08,transparent_70%)]\" />\n\n              {content}\n            </div>\n          </div>\n        ) : (\n          <>\n            {/* Ambient Top Subtle Background Lighting & Radial Glow */}\n            <div className=\"pointer-events-none absolute -top-28 left-1/2 -translate-x-1/2 size-[640px] rounded-full bg-primary/10 blur-[130px] opacity-75\" />\n            <div className=\"pointer-events-none absolute top-0 inset-x-0 h-96 bg-gradient-to-b from-primary/[0.06] via-primary/[0.02] to-transparent\" />\n            <div className=\"pointer-events-none absolute top-12 left-1/2 -translate-x-1/2 w-full max-w-5xl h-64 bg-[radial-gradient(ellipse_at_top,var(--primary)/0.08,transparent_70%)]\" />\n\n            {content}\n          </>\n        )}\n      </section>\n    );\n  }\n);\n\nBreatheHero.displayName = \"BreatheHero\";\n\nexport default BreatheHero;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/BreatheHeroBlock.tsx"
    },
    {
      "path": "src/lib/accent.ts",
      "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Bridging the docs colour picker into a preview.\n *\n * Most blocks are already written against the `--primary` token (`bg-primary`,\n * `ring-primary/30`, `text-primary-foreground`…). Rather than teaching each one a\n * bespoke `color` prop, a demo can repoint that token for its own subtree and\n * every one of those classes follows along.\n */\n\n/** The swatches the picker offers, as concrete CSS colours. */\nconst PRESET_HEX: Record<string, string> = {\n  lavender: \"#9b8afb\",\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n  zinc: \"#18181b\",\n};\n\n/** True when the picker sits on \"Default\" — the page theme should win. */\nexport function isDefaultAccent(color?: string): boolean {\n  return !color || color === \"primary\" || color === \"default\";\n}\n\n/**\n * Turns a picker value into a CSS colour, or null for \"Default\" — meaning\n * nothing should be overridden, so the theme (and the sidebar customizer)\n * keeps control.\n */\nexport function resolveAccent(color?: string): string | null {\n  if (isDefaultAccent(color)) return null;\n  return PRESET_HEX[color as string] ?? (color as string);\n}\n\n/**\n * The picker value in a form every block accepts. Blocks know the older swatch\n * names but not \"lavender\" — and since that is also a CSS named colour\n * (#e6e6fa), a block that passes unknown names straight through would paint\n * the wrong shade, while others fall back to a different palette. Demos that\n * hand the picker value to such a block run it through this first. Every\n * other value is returned untouched.\n */\nexport function toBlockColor(color: string): string;\nexport function toBlockColor(color: string | undefined): string | undefined;\nexport function toBlockColor(color?: string): string | undefined {\n  return color === \"lavender\" ? PRESET_HEX.lavender : color;\n}\n\nfunction parseHex(input: string): [number, number, number] | null {\n  const hex = input.trim().replace(/^#/, \"\");\n  if (/^[a-f\\d]{3}$/i.test(hex)) {\n    const [r, g, b] = hex.split(\"\");\n    return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];\n  }\n  if (/^[a-f\\d]{6}$/i.test(hex)) {\n    return [\n      parseInt(hex.slice(0, 2), 16),\n      parseInt(hex.slice(2, 4), 16),\n      parseInt(hex.slice(4, 6), 16),\n    ];\n  }\n  return null;\n}\n\n/** Legible foreground for content sitting on a filled `color` swatch. */\nexport function accentForeground(color: string): string {\n  const rgb = parseHex(color);\n  if (!rgb) return \"#ffffff\";\n  const channel = (v: number) => {\n    const c = v / 255;\n    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n  };\n  const luminance =\n    0.2126 * channel(rgb[0]) +\n    0.7152 * channel(rgb[1]) +\n    0.0722 * channel(rgb[2]);\n  return luminance > 0.45 ? \"#09090b\" : \"#ffffff\";\n}\n\n/** The theme tokens a picked accent repoints, as `--name` → value. */\nexport function accentTokens(color?: string): Record<string, string> {\n  const resolved = resolveAccent(color);\n  if (!resolved) return {};\n  return {\n    \"--primary\": resolved,\n    \"--primary-foreground\": accentForeground(resolved),\n    \"--ring\": resolved,\n  };\n}\n\n/**\n * Inline style repointing `--primary` at the picked accent for one subtree.\n * `--primary-foreground` is derived alongside it, otherwise a light pick leaves\n * `text-primary-foreground` content unreadable on its own fill. Returns `{}` on\n * \"Default\" so the page theme still wins.\n */\nexport function accentStyle(color?: string): CSSProperties {\n  return accentTokens(color) as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "lib/accent.ts"
    },
    {
      "path": "src/lib/animate.ts",
      "content": "import { useEffect, useState, useSyncExternalStore, type RefObject } from \"react\";\n\n/**\n * Animation helpers for components that need no animation library: spring\n * easings for CSS transitions and the Web Animations API, plus the two browser\n * checks those components share. Installed alongside any component that uses it.\n */\n\nexport type SpringOptions = {\n  stiffness?: number;\n  damping?: number;\n  mass?: number;\n};\n\n/** A spring as a CSS timing: `duration` in milliseconds, `easing` as `linear(…)`. */\nexport type Timing = { duration: number; easing: string };\n\n/** How many points of the spring's curve the easing samples. */\nconst SAMPLES = 48;\n\n/** The time constant multiple at which a spring is within 0.1% of its target. */\nconst SETTLED = Math.log(1000);\n\n/**\n * Browsers without `linear()` get a plain ease instead: the Web Animations API\n * throws on an easing it can't parse, rather than ignoring it as CSS would.\n */\nconst supportsLinearEasing = () =>\n  typeof CSS === \"undefined\" || CSS.supports(\"transition-timing-function\", \"linear(0, 1)\");\n\n/** The response of a spring released from rest, as a CSS easing. */\nexport function springTiming({ stiffness = 100, damping = 10, mass = 1 }: SpringOptions = {}): Timing {\n  const natural = Math.sqrt(stiffness / mass);\n  const ratio = Math.min(damping / (2 * Math.sqrt(stiffness * mass)), 1);\n  const decay = ratio * natural;\n  const seconds = SETTLED / decay;\n  if (!supportsLinearEasing()) return { duration: Math.round(seconds * 1000), easing: \"ease-out\" };\n\n  const damped = natural * Math.sqrt(1 - ratio * ratio);\n  const position = (t: number) =>\n    ratio < 1\n      ? 1 - Math.exp(-decay * t) * (Math.cos(damped * t) + (decay / damped) * Math.sin(damped * t))\n      : 1 - Math.exp(-natural * t) * (1 + natural * t);\n\n  const points = Array.from({ length: SAMPLES + 1 }, (_, i) =>\n    i === SAMPLES ? \"1\" : position((i / SAMPLES) * seconds).toFixed(4),\n  );\n  return { duration: Math.round(seconds * 1000), easing: `linear(${points.join(\",\")})` };\n}\n\n/** A spring described by how long it takes and how much it bounces (0 to 1). */\nexport function bounceTiming(seconds: number, bounce: number): Timing {\n  const ratio = 1 - bounce;\n  const natural = SETTLED / (ratio * seconds);\n  return springTiming({ stiffness: natural * natural, damping: 2 * ratio * natural });\n}\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\n\nconst subscribeToReducedMotion = (onChange: () => void) => {\n  const query = window.matchMedia(REDUCED_MOTION);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\n\n/** Whether the visitor asked for less motion. False during server rendering. */\nexport const usePrefersReducedMotion = () =>\n  useSyncExternalStore(\n    subscribeToReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  );\n\n/** The same check, for code running outside render. */\nexport const prefersReducedMotion = () =>\n  typeof window !== \"undefined\" && window.matchMedia(REDUCED_MOTION).matches;\n\n/**\n * Whether an element is on screen. `amount` is the share of it that must show.\n * With `once` it stays true after the first time.\n */\nexport function useInView(\n  ref: RefObject<Element | null>,\n  { amount = 0, once = false }: { amount?: number; once?: boolean } = {},\n): boolean {\n  const [inView, setInView] = useState(false);\n\n  useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        setInView(entry.isIntersecting);\n        if (entry.isIntersecting && once) observer.disconnect();\n      },\n      { threshold: amount },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [ref, amount, once]);\n\n  return inView;\n}\n",
      "type": "registry:component",
      "target": "lib/animate.ts"
    }
  ],
  "docs": "Named export: BreatheHero (also available as variant='breathe' in HeroBlock). Pass `title`, `titleAccent`, `description`, `buttonLabel`, `buttonHref`, `cards`, `images`, `scrollMode`, `scrollScale`, `stagger`, `captions`, and `accent`.",
  "categories": [
    "marketing",
    "layout",
    "scroll"
  ],
  "type": "registry:component"
}
