{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gallery",
  "title": "Gallery",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 6 gallery variants: https://avaraui.com/view/gallery. This item holds 6 of them as looks of one component (orbit-gallery, circular-reel, video-sphere, moving-bento, pill-carousel, arc-carousel). Skip the question if the user already named one. A versatile, multi-variant interactive visual gallery featuring revolving 3D orbit rings, responsive bento grids, vertical continuous reels, 3D pill carousels, and perspective arc carousels. Use it for agencies, portfolios, product showcases, and creative hero sections.",
  "dependencies": [
    "gsap",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/GalleryBlock.tsx",
      "content": "\"use client\";\n\nimport React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, memo } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Aperture, ArrowUpRight, Camera, Compass, MapPin, Sparkles, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport gsap from \"gsap\";\nimport { Flip } from \"gsap/dist/Flip\";\n\nif (typeof window !== \"undefined\") {\n  gsap.registerPlugin(Flip);\n}\n\nconst useIsomorphicLayoutEffect = typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\n// ==========================================\n// ORBIT GALLERY, MOVING BENTO & CIRCULAR REEL\n// A dynamic visual showcase featuring:\n// 1. Orbit Ring: revolving 3D circular photo carousel with upright rotation and mouse trail.\n// 2. Moving Bento: self-rearranging image bento grid with a dominant center hero slot,\n//    powered by fluid GSAP Flip transitions.\n// 3. Circular Reel: interactive scroll-driven dial with passive rotation,\n//    tangential segmented ring slices, and auto-crossfading preview.\n// ==========================================\n\nexport interface OrbitHighlight {\n  label: string;\n  /** Colour of the dot before the label; the accent by default. */\n  color?: string;\n}\n\nexport interface MovingBentoItem {\n  id: string;\n  title: string;\n  description?: string;\n  image: string;\n}\n\nexport interface MovingBentoProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: MovingBentoItem[];\n  swapInterval?: number;\n  gap?: number;\n  autoPlay?: boolean;\n  color?: string;\n  onCardClick?: (item: MovingBentoItem, index: number) => void;\n}\n\nexport interface CircularReelItem {\n  id?: string;\n  name: string;\n  category?: string;\n  description?: string;\n  image: string;\n  href?: string;\n}\n\nexport interface CircularReelProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: CircularReelItem[];\n  color?: string;\n  accentColor?: string;\n  backgroundColor?: string;\n  idleRotationSpeed?: number;\n  scrollSensitivity?: number;\n  ringThickness?: number;\n  fontSize?: number;\n  grayscale?: boolean;\n  autoPlay?: boolean;\n  onItemChange?: (item: CircularReelItem, index: number) => void;\n}\n\nexport interface PillCarouselItem {\n  id?: string;\n  image: string | { src: string; alt?: string };\n  tooltipText?: string;\n  link?: string;\n}\n\nexport interface PillCarouselProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: PillCarouselItem[];\n  color?: string;\n  backgroundColor?: string;\n  cardBorderRadius?: number;\n  cardShadow?: string;\n  activeBorderWidth?: number;\n  activeBorderColor?: string;\n  transitionDuration?: number;\n  hoverScale?: number;\n  hoverBrightness?: number;\n  /** Controls whether pills slide automatically (true) or only when clicked (false). Defaults to true. */\n  autoSlide?: boolean;\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  tooltipText?: string;\n  tooltipBackgroundColor?: string;\n  tooltipTextColor?: string;\n  tooltipPadding?: string;\n  tooltipFontSize?: number;\n  tooltipBorderRadius?: number;\n  tooltipShadow?: string;\n  linkNewTab?: boolean;\n  onTooltipClick?: (item: PillCarouselItem, index: number) => void;\n}\n\nexport interface ArcCarouselItem {\n  id?: string;\n  image: string | { src: string; alt?: string };\n  title?: string;\n  subtitle?: string;\n  link?: string;\n}\n\nexport interface ArcCarouselProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: ArcCarouselItem[];\n  color?: string;\n  backgroundColor?: string;\n  cardSize?: number;\n  cardHeight?: number;\n  gap?: number;\n  /** Curvature value from -300 to 300 (defaults to -200 for a concave arc). */\n  curve?: number;\n  perspective?: number;\n  autoScrollSpeed?: number;\n  autoSlide?: boolean;\n  autoPlay?: boolean;\n  dragSpeed?: number;\n  inertia?: number;\n  borderRadius?: number;\n  linkNewTab?: boolean;\n  onCardClick?: (item: ArcCarouselItem, index: number) => void;\n}\n\nexport interface VideoSphereItem {\n  id?: string;\n  /** Media type: \"image\" or \"video\". If omitted, inferred from file extension. */\n  type?: \"image\" | \"video\";\n  src: string;\n  poster?: string;\n  title?: string;\n  description?: string;\n  link?: string;\n}\n\nexport interface VideoSphereGalleryProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: VideoSphereItem[];\n  radius?: number;\n  itemCount?: number;\n  autoRotateSpeed?: number;\n  itemWidth?: number;\n  itemHeight?: number;\n  perspective?: number;\n  blurStrength?: number;\n  autoRotate?: boolean;\n  muted?: boolean;\n  loop?: boolean;\n  unmuteOnHover?: boolean;\n  maxConcurrentVideos?: number;\n  hoverScale?: number;\n  enableHoverScale?: boolean;\n  enablePopup?: boolean;\n  popupWidth?: number;\n  popupHeight?: number;\n  cardBorderColor?: string;\n  color?: string;\n  onCardClick?: (item: VideoSphereItem, index: number) => void;\n}\n\nexport interface GalleryProps extends React.HTMLAttributes<HTMLElement> {\n  /** Gallery variant: \"orbit\" | \"orbit-gallery\" (circular revolving ring), \"moving-bento\" (GSAP Flip self-rearranging grid), \"circular-reel\" (interactive scroll-driven spinner), \"pill-carousel\" (3D layered pill carousel), \"arc-carousel\" / \"arc-carousel-gallery\" (3D cylindrical arc carousel), or \"video-sphere\" / \"video-sphere-gallery\" (interactive 3D video sphere showcase). */\n  variant?: \"orbit\" | \"orbit-gallery\" | \"moving-bento\" | \"circular-reel\" | \"pill-carousel\" | \"arc-carousel\" | \"arc-carousel-gallery\" | \"video-sphere\" | \"video-sphere-gallery\";\n  /** The photographs on the orbit ring, in orbit order. */\n  images?: string[];\n  /** Seconds for one full revolution of the orbit ring. */\n  duration?: number;\n  /** Accent colour — a preset name (indigo, rose, emerald, zinc) or any CSS colour. */\n  color?: string;\n\n  // Moving Bento specific props\n  /** Bento items for the \"moving-bento\" variant. */\n  items?: MovingBentoItem[];\n  /** Swap interval in ms for the bento variant. */\n  swapInterval?: number;\n  /** Grid gap in px for the bento variant. */\n  gap?: number;\n  /** Auto-swapping state for the bento / reel / pill-carousel / arc-carousel variants. */\n  autoPlay?: boolean;\n\n  // Circular Reel specific props\n  /** Reel items for the \"circular-reel\" variant. */\n  reelItems?: CircularReelItem[];\n  /** Degrees per second of idle passive rotation. */\n  idleRotationSpeed?: number;\n  /** Wheel / scroll delta multiplier. */\n  scrollSensitivity?: number;\n  /** Donut ring thickness percentage (10 to 60). */\n  ringThickness?: number;\n  /** Filter images with grayscale preview. */\n  grayscale?: boolean;\n\n  // 3D Pill Carousel specific props\n  /** Pill items for the \"pill-carousel\" variant. */\n  pillItems?: PillCarouselItem[];\n  /** Pill card border radius in px. */\n  cardBorderRadius?: number;\n  /** Active center pill border width in px. */\n  activeBorderWidth?: number;\n  /** Active center pill border color (defaults to theme primary/accent). */\n  activeBorderColor?: string;\n  /** Transition duration in seconds for pill movement. */\n  transitionDuration?: number;\n  /** Scale multiplier when hovering peripheral pills. */\n  hoverScale?: number;\n  /** Brightness multiplier when hovering peripheral pills. */\n  hoverBrightness?: number;\n  /** Controls whether pills slide automatically (true) or only on click (false). Defaults to true. */\n  autoSlide?: boolean;\n  /** Auto-play interval in ms for the pill carousel. */\n  autoPlayInterval?: number;\n  /** Default fallback tooltip CTA text. */\n  tooltipText?: string;\n  /** Tooltip background color (defaults to theme primary/accent). */\n  tooltipBackgroundColor?: string;\n  /** Tooltip text color (defaults to theme primary-foreground). */\n  tooltipTextColor?: string;\n\n  // Arc Carousel specific props\n  /** Items for the \"arc-carousel\" variant. */\n  arcItems?: ArcCarouselItem[];\n  /** Width of each card in px (defaults to 220). */\n  cardSize?: number;\n  /** Height of each card in px (defaults to cardSize * 1.32). */\n  cardHeight?: number;\n  /** Curvature value from -300 to 300 (negative is concave, 0 is flat, positive is convex). Defaults to -200. */\n  curve?: number;\n  /** 3D perspective depth in px (defaults to 1200). */\n  perspective?: number;\n  /** Auto-scroll rotation speed (defaults to 0.22). */\n  autoScrollSpeed?: number;\n  /** Drag sensitivity multiplier (defaults to 1.2). */\n  dragSpeed?: number;\n  /** Momentum inertia friction (0 to 1, defaults to 0.75). */\n  inertia?: number;\n  /** Card border radius in px for the arc carousel. */\n  borderRadius?: number;\n\n  // Video Sphere specific props\n  /** Items for the \"video-sphere\" variant. */\n  sphereItems?: VideoSphereItem[];\n  /** Sphere radius in px (defaults to 280). */\n  radius?: number;\n  /** Number of cards distributed around the sphere (defaults to 30). */\n  itemCount?: number;\n  /** Speed of passive auto-rotation drift (defaults to 0.0015). */\n  autoRotateSpeed?: number;\n  /** Card width in px (defaults to 120). */\n  itemWidth?: number;\n  /** Card height in px (defaults to 165). */\n  itemHeight?: number;\n  /** Max blur in px for cards on the far side of the sphere (defaults to 12). */\n  blurStrength?: number;\n  /** Whether to unmute audio on card hover (defaults to false). */\n  unmuteOnHover?: boolean;\n  /** Maximum number of front-facing videos actively playing at once (defaults to 12). */\n  maxConcurrentVideos?: number;\n  /** Whether hovering zooms a card (defaults to true). */\n  enableHoverScale?: boolean;\n  /** Whether clicking opens a popup preview modal (defaults to true). */\n  enablePopup?: boolean;\n  /** Width in px of popup modal (defaults to 480). */\n  popupWidth?: number;\n  /** Height in px of popup modal (defaults to 500). */\n  popupHeight?: number;\n  /** Border color for sphere cards (defaults to theme color var(--primary)). */\n  cardBorderColor?: string;\n  /** Click callback for gallery cards. */\n  onCardClick?: (item: any, index: number) => void;\n}\n\nexport type OrbitGalleryProps = GalleryProps;\n\n// ------------------------------------------\n// Accent colour: keep it readable on both light and dark surfaces.\n// ------------------------------------------\ntype Rgb = [number, number, number];\n\nconst LIGHT_SURFACE_LUM = 1;\nconst DARK_SURFACE_LUM = 0.003;\nconst MIN_CONTRAST = 3;\n\nconst parseColor = (input: string): Rgb | 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 [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];\n  }\n  const m = input.match(/^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/i);\n  return m ? [+m[1], +m[2], +m[3]] : null;\n};\n\nconst relLuminance = ([r, g, b]: Rgb) => {\n  const f = (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  return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);\n};\n\nconst contrast = (a: number, b: number) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n\nconst ensureContrast = (rgb: Rgb, surfaceLum: number, target: 0 | 255): Rgb => {\n  let out = rgb;\n  for (let i = 0; i < 16; i++) {\n    if (contrast(relLuminance(out), surfaceLum) >= MIN_CONTRAST) break;\n    out = out.map((v) => Math.round(v + (target - v) * 0.12)) as Rgb;\n  }\n  return out;\n};\n\nconst toCss = ([r, g, b]: Rgb) => `rgb(${r}, ${g}, ${b})`;\n\nconst PRESETS: Record<string, string> = {\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n  zinc: \"#18181b\",\n};\n\nfunction accentStyle(color?: string): React.CSSProperties {\n  const resolved = !color || color === \"primary\" || color === \"default\" ? undefined : (PRESETS[color] ?? color);\n  const rgb = resolved ? parseColor(resolved) : null;\n  return {\n    \"--hero-accent-text\": rgb\n      ? `light-dark(${toCss(ensureContrast(rgb, LIGHT_SURFACE_LUM, 0))}, ${toCss(ensureContrast(rgb, DARK_SURFACE_LUM, 255))})`\n      : \"var(--primary)\",\n    ...(resolved ? { \"--primary\": resolved } : {}),\n    ...(rgb ? { \"--primary-foreground\": relLuminance(rgb) > 0.45 ? \"#09090b\" : \"#ffffff\" } : {}),\n  } as React.CSSProperties;\n}\n\n// ------------------------------------------\n// Defaults\n// ------------------------------------------\nconst DEFAULT_IMAGES = [\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\", // Yosemite\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\", // Sunlit forest path\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\", // Mountain fog\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\", // Forest sunbeam\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\", // Green valley\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\", // Mountain lake\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\", // Wilderness\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\", // Peak at sunset\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790148863/photo-1447752875215-b2761acb3c5d-1790148865109.jpg\", // Autumn stream\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790146210/photo-1507525428034-b723cf961d3e-1790146212208.jpg\", // Tropical coast\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790158813/photo-1433086966358-54859d0ed716-1790158812209.jpg\", // Waterfall\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\", // Alps\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790145142/photo-1500530855697-b586d89ba3ee-1790145143493.jpg\", // Golden lake\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790148863/photo-1473448912268-2022ce9509d8-1790148865110.jpg\", // Pine forest\n  \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_600,c_limit/v1790158814/photo-1518495973542-4542c06a5843-1790158812213.jpg\", // Sunlit trees\n];\n\nexport const DEFAULT_BENTO_ITEMS: MovingBentoItem[] = [\n  {\n    id: \"alpine-horizons\",\n    title: \"Alpine Horizons\",\n    description: \"Granite monoliths rising above early morning valley fog\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n  },\n  {\n    id: \"emerald-canopy\",\n    title: \"Emerald Canopy\",\n    description: \"Sunlit trails winding through ancient coastal redwood forests\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n  },\n  {\n    id: \"whispering-peaks\",\n    title: \"Whispering Peaks\",\n    description: \"Layers of mountain silhouettes veiled in twilight atmospheric mist\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n  },\n  {\n    id: \"sunbeam-sanctuary\",\n    title: \"Sunbeam Sanctuary\",\n    description: \"Golden rays breaking through tall evergreens after summer rain\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n  },\n  {\n    id: \"verdant-valleys\",\n    title: \"Verdant Valleys\",\n    description: \"Rolling emerald highlands sweeping toward dramatic skies\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\",\n  },\n  {\n    id: \"glacial-waters\",\n    title: \"Glacial Reflections\",\n    description: \"Mirror-still alpine lake echoing snow-dusted jagged ridge lines\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n  },\n  {\n    id: \"untamed-wilds\",\n    title: \"Untamed Wilds\",\n    description: \"Untouched wilderness under vibrant open skies\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\",\n  },\n  {\n    id: \"golden-ridge\",\n    title: \"Golden Ridge\",\n    description: \"Late sunset warmth illuminating steep rocky summits\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n  },\n];\n\nexport const DEFAULT_HIGHLIGHTS: OrbitHighlight[] = [\n  { label: \"15 hand-picked scenes\" },\n  { label: \"Full-circle rotation\", color: \"#10b981\" },\n  { label: \"Holds still on hover\", color: \"#f59e0b\" },\n];\n\nexport const DEFAULT_BENTO_HIGHLIGHTS: OrbitHighlight[] = [\n  { label: \"GSAP Flip layout animation\" },\n  { label: \"Self-rearranging hero card\", color: \"#10b981\" },\n  { label: \"Click to focus\", color: \"#f59e0b\" },\n];\n\nexport const DEFAULT_CIRCULAR_REEL_ITEMS: CircularReelItem[] = [\n  {\n    id: \"reel-1\",\n    name: \"Velvet Horizon\",\n    category: \"Yosemite Valley\",\n    description: \"Granite peaks glowing in the golden twilight above misty valley floor\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n  },\n  {\n    id: \"reel-2\",\n    name: \"Glass Relay\",\n    category: \"Redwood Trail\",\n    description: \"Ancient sequoias towering through filtered coastal morning rays\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n  },\n  {\n    id: \"reel-3\",\n    name: \"Noir Mist\",\n    category: \"Mountain Peaks\",\n    description: \"Monochrome layered ridges dissolving into distant cloud banks\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n  },\n  {\n    id: \"reel-4\",\n    name: \"Driftline Rays\",\n    category: \"Canopy Light\",\n    description: \"Luminescent beams descending through mossy pine canopies\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n  },\n  {\n    id: \"reel-5\",\n    name: \"Pulse Emerald\",\n    category: \"Verdant Highlands\",\n    description: \"Vibrant rolling green bluffs descending toward serene shoreline\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\",\n  },\n  {\n    id: \"reel-6\",\n    name: \"Cold Meridian\",\n    category: \"Alpine Lake\",\n    description: \"Crystalline glacial waters reflecting jagged snow-capped crests\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n  },\n  {\n    id: \"reel-7\",\n    name: \"Astra Wilds\",\n    category: \"Open Range\",\n    description: \"Solitary wanderer contemplating vast expanses of pristine tundra\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\",\n  },\n  {\n    id: \"reel-8\",\n    name: \"Mono Summit\",\n    category: \"Golden Ridge\",\n    description: \"Sharp stone pinnacles bathed in amber warmth at sunrise\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n  },\n  {\n    id: \"reel-9\",\n    name: \"Lumen Cascade\",\n    category: \"Forest Waterfall\",\n    description: \"Rushing mountain falls carving through lush temperate rainforest\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790158813/photo-1433086966358-54859d0ed716-1790158812209.jpg\",\n  },\n  {\n    id: \"reel-10\",\n    name: \"Shadow Bloom\",\n    category: \"Alpine Crest\",\n    description: \"Majestic European peaks framed by drifting atmospheric shadows\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n  },\n];\n\nexport const DEFAULT_REEL_HIGHLIGHTS: OrbitHighlight[] = [\n  { label: \"Interactive circular dial\" },\n  { label: \"Scroll & drag to spin\", color: \"#10b981\" },\n  { label: \"Auto-crossfading preview\", color: \"#f59e0b\" },\n];\n\nexport const DEFAULT_PILL_CAROUSEL_ITEMS: PillCarouselItem[] = [\n  {\n    id: \"pill-1\",\n    image: {\n      src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n      alt: \"Daily Rituals\",\n    },\n    tooltipText: \"Daily Rituals\",\n    link: \"\",\n  },\n  {\n    id: \"pill-2\",\n    image: {\n      src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n      alt: \"Slow Living\",\n    },\n    tooltipText: \"Slow Living\",\n    link: \"\",\n  },\n  {\n    id: \"pill-3\",\n    image: {\n      src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n      alt: \"Explore\",\n    },\n    tooltipText: \"Explore\",\n    link: \"\",\n  },\n  {\n    id: \"pill-4\",\n    image: {\n      src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n      alt: \"Morning Flow\",\n    },\n    tooltipText: \"Morning Flow\",\n    link: \"\",\n  },\n  {\n    id: \"pill-5\",\n    image: {\n      src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n      alt: \"Soft Focus\",\n    },\n    tooltipText: \"Soft Focus\",\n    link: \"\",\n  },\n];\n\nexport const DEFAULT_PILL_HIGHLIGHTS: OrbitHighlight[] = [\n  { label: \"3D layered pill cards\" },\n  { label: \"Click to focus & keyboard nav\", color: \"#10b981\" },\n  { label: \"Animated tooltip CTA\", color: \"#f59e0b\" },\n];\n\nexport const DEFAULT_ARC_CAROUSEL_ITEMS: ArcCarouselItem[] = [\n  {\n    id: \"arc-1\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n    title: \"Velvet Horizon\",\n    subtitle: \"Yosemite Valley\",\n  },\n  {\n    id: \"arc-2\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n    title: \"Glass Relay\",\n    subtitle: \"Redwood Trail\",\n  },\n  {\n    id: \"arc-3\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n    title: \"Noir Mist\",\n    subtitle: \"Mountain Peaks\",\n  },\n  {\n    id: \"arc-4\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n    title: \"Driftline Rays\",\n    subtitle: \"Canopy Light\",\n  },\n  {\n    id: \"arc-5\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\",\n    title: \"Pulse Emerald\",\n    subtitle: \"Verdant Highlands\",\n  },\n  {\n    id: \"arc-6\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n    title: \"Cold Meridian\",\n    subtitle: \"Alpine Lake\",\n  },\n  {\n    id: \"arc-7\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\",\n    title: \"Astra Wilds\",\n    subtitle: \"Open Range\",\n  },\n  {\n    id: \"arc-8\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n    title: \"Mono Summit\",\n    subtitle: \"Golden Ridge\",\n  },\n  {\n    id: \"arc-9\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790158813/photo-1433086966358-54859d0ed716-1790158812209.jpg\",\n    title: \"Lumen Cascade\",\n    subtitle: \"Forest Waterfall\",\n  },\n  {\n    id: \"arc-10\",\n    image: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n    title: \"Shadow Bloom\",\n    subtitle: \"Alpine Crest\",\n  },\n];\n\nexport const DEFAULT_ARC_HIGHLIGHTS: OrbitHighlight[] = [\n  { label: \"3D Cylindrical Arc\" },\n  { label: \"Momentum Physics\", color: \"#10b981\" },\n  { label: \"Concave Curvature\", color: \"#f59e0b\" },\n];\n\nexport const DEFAULT_VIDEO_SPHERE_ITEMS: VideoSphereItem[] = [\n  {\n    id: \"sphere-1\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n    title: \"Alpine Horizons\",\n    description: \"Granite monoliths rising above early morning valley fog\",\n  },\n  {\n    id: \"sphere-2\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n    title: \"Emerald Canopy\",\n    description: \"Sunlit trails winding through ancient coastal redwood forests\",\n  },\n  {\n    id: \"sphere-3\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n    title: \"Whispering Peaks\",\n    description: \"Layers of mountain silhouettes veiled in twilight atmospheric mist\",\n  },\n  {\n    id: \"sphere-4\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146211/photo-1441974231531-c6227db76b6e-1790146212212.jpg\",\n    title: \"Sunbeam Sanctuary\",\n    description: \"Golden rays breaking through tall evergreens after summer rain\",\n  },\n  {\n    id: \"sphere-5\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1472214103451-9374bd1c798e-1790146212205.jpg\",\n    title: \"Verdant Valleys\",\n    description: \"Rolling emerald highlands sweeping toward dramatic skies\",\n  },\n  {\n    id: \"sphere-6\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1501785888041-af3ef285b470-1790146212207.jpg\",\n    title: \"Glacial Reflections\",\n    description: \"Mirror-still alpine lake echoing snow-dusted jagged ridge lines\",\n  },\n  {\n    id: \"sphere-7\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146209/photo-1469474968028-56623f02e42e-1790146212205.jpg\",\n    title: \"Untamed Wilds\",\n    description: \"Untouched wilderness under vibrant open skies\",\n  },\n  {\n    id: \"sphere-8\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n    title: \"Golden Hour Ridge\",\n    description: \"Sun setting behind sharp mountain contours\",\n  },\n  {\n    id: \"sphere-9\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790148863/photo-1447752875215-b2761acb3c5d-1790148865109.jpg\",\n    title: \"Autumn Stream\",\n    description: \"Gentle brook weaving through vibrant autumn woods\",\n  },\n  {\n    id: \"sphere-10\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790146210/photo-1507525428034-b723cf961d3e-1790146212208.jpg\",\n    title: \"Tropical Coast\",\n    description: \"Crystal waters washing onto warm golden sands\",\n  },\n  {\n    id: \"sphere-11\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790158813/photo-1433086966358-54859d0ed716-1790158812209.jpg\",\n    title: \"Cascading Falls\",\n    description: \"Hidden waterfall plunging into clear mountain pools\",\n  },\n  {\n    id: \"sphere-12\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n    title: \"Alpine Mist\",\n    description: \"Snow-covered peaks shrouded in high-altitude clouds\",\n  },\n  {\n    id: \"sphere-13\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790145142/photo-1500530855697-b586d89ba3ee-1790145143493.jpg\",\n    title: \"Golden Waters\",\n    description: \"Sunset glow reflecting over tranquil lake waters\",\n  },\n  {\n    id: \"sphere-14\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790148863/photo-1473448912268-2022ce9509d8-1790148865110.jpg\",\n    title: \"Pine Forest\",\n    description: \"Lush green pine trees greeting early morning light\",\n  },\n  {\n    id: \"sphere-15\",\n    type: \"image\",\n    src: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_800,c_limit/v1790158814/photo-1518495973542-4542c06a5843-1790158812213.jpg\",\n    title: \"Sunlit Trees\",\n    description: \"Dappled sunlight filtering through summer leaves\",\n  },\n];\n\n\n// ==========================================\n// MOVING BENTO GRID DEFINITIONS\n// ==========================================\ninterface SlotDef {\n  gridColumn: string;\n  gridRow: string;\n}\n\nconst DESKTOP_SLOTS: SlotDef[] = [\n  { gridColumn: \"2 / 4\", gridRow: \"2 / 4\" }, // CENTER HERO (2x2)\n  { gridColumn: \"1 / 2\", gridRow: \"1 / 2\" }, // top-left\n  { gridColumn: \"2 / 4\", gridRow: \"1 / 2\" }, // top-center wide\n  { gridColumn: \"4 / 5\", gridRow: \"1 / 2\" }, // top-right\n  { gridColumn: \"1 / 2\", gridRow: \"2 / 3\" }, // mid-left\n  { gridColumn: \"4 / 5\", gridRow: \"2 / 3\" }, // mid-right\n  { gridColumn: \"1 / 2\", gridRow: \"3 / 4\" }, // bottom-left\n  { gridColumn: \"4 / 5\", gridRow: \"3 / 4\" }, // bottom-right\n];\n\nconst TABLET_SLOTS: SlotDef[] = [\n  { gridColumn: \"2 / 4\", gridRow: \"2 / 4\" }, // Hero (2x2)\n  { gridColumn: \"1 / 2\", gridRow: \"1 / 2\" },\n  { gridColumn: \"2 / 3\", gridRow: \"1 / 2\" },\n  { gridColumn: \"3 / 4\", gridRow: \"1 / 2\" },\n  { gridColumn: \"1 / 2\", gridRow: \"2 / 3\" },\n  { gridColumn: \"1 / 2\", gridRow: \"3 / 4\" },\n];\n\nconst MOBILE_SLOTS: SlotDef[] = [\n  { gridColumn: \"1 / 3\", gridRow: \"2 / 3\" }, // Hero middle row (2 cols x 1 row)\n  { gridColumn: \"1 / 2\", gridRow: \"1 / 2\" },\n  { gridColumn: \"2 / 3\", gridRow: \"1 / 2\" },\n  { gridColumn: \"1 / 2\", gridRow: \"3 / 4\" },\n  { gridColumn: \"2 / 3\", gridRow: \"3 / 4\" },\n];\n\nconst SLOT_COUNTS = { mobile: 5, tablet: 6, desktop: 8 } as const;\ntype Breakpoint = keyof typeof SLOT_COUNTS;\n\nconst SLOTS: Record<Breakpoint, SlotDef[]> = {\n  mobile: MOBILE_SLOTS,\n  tablet: TABLET_SLOTS,\n  desktop: DESKTOP_SLOTS,\n};\n\nconst GRID_COLS: Record<Breakpoint, number> = { mobile: 2, tablet: 3, desktop: 4 };\nconst ASPECT_RATIOS: Record<Breakpoint, string> = {\n  mobile: \"3 / 4\",\n  tablet: \"4 / 3\",\n  desktop: \"16 / 10\",\n};\n\nconst CENTER_SLOT = 0;\nconst DATA_ATTR = \"data-moving-id\";\nconst DATA_SELECTOR = `[${DATA_ATTR}]`;\n\nfunction pickPeripheral(count: number): number {\n  return 1 + Math.floor(Math.random() * (count - 1));\n}\n\n// ------------------------------------------\n// Bento Card Component\n// ------------------------------------------\nconst BentoCard = memo(function BentoCard({\n  item,\n  isHero,\n  onClick,\n}: {\n  item: MovingBentoItem;\n  isHero?: boolean;\n  onClick?: () => void;\n}) {\n  return (\n    <div\n      onClick={onClick}\n      className={cn(\n        \"group relative size-full overflow-hidden rounded-2xl select-none transition-all duration-300\",\n        \"border border-border/50 bg-card shadow-md hover:shadow-xl\",\n        !isHero && \"cursor-pointer hover:border-primary/50 hover:brightness-105 active:scale-[0.98]\",\n        isHero && \"ring-1 ring-primary/30 shadow-2xl\"\n      )}\n    >\n      <div\n        className=\"absolute inset-0 bg-cover bg-center transition-transform duration-700 ease-out group-hover:scale-105\"\n        style={{ backgroundImage: `url(${item.image})` }}\n      />\n      <div\n        className=\"absolute inset-0 bg-gradient-to-t from-black/85 via-black/25 to-transparent transition-opacity duration-300\"\n        aria-hidden=\"true\"\n      />\n\n      <div className=\"relative size-full flex flex-col justify-end p-2.5 sm:p-4 md:p-5 z-10 pointer-events-none\">\n        <h3 className=\"text-xs sm:text-sm md:text-base font-bold text-white leading-tight drop-shadow-sm\">\n          {item.title}\n        </h3>\n        {item.description && (\n          <p className=\"text-[10px] sm:text-xs text-white/80 mt-0.5 sm:mt-1 leading-relaxed drop-shadow-sm line-clamp-1 sm:line-clamp-2\">\n            {item.description}\n          </p>\n        )}\n      </div>\n    </div>\n  );\n});\n\n// ------------------------------------------\n// Moving Bento Standalone Component\n// ------------------------------------------\nexport function MovingBento({\n  items = DEFAULT_BENTO_ITEMS,\n  swapInterval = 3000,\n  gap = 12,\n  autoPlay = true,\n  color,\n  className,\n  style,\n  onCardClick,\n  ...props\n}: MovingBentoProps) {\n  const gridRef = useRef<HTMLDivElement>(null);\n  const [bp, setBp] = useState<Breakpoint>(\"desktop\");\n  const isHovered = useRef(false);\n  const isOffScreen = useRef(false);\n  const isAnimating = useRef(false);\n  const hasEntered = useRef(false);\n  const flipStateRef = useRef<Flip.FlipState | null>(null);\n  const flipTweenRef = useRef<gsap.core.Timeline | gsap.core.Tween | null>(null);\n\n  const [assignment, setAssignment] = useState<number[]>(() =>\n    Array.from({ length: Math.max(items.length, SLOT_COUNTS.desktop) }, (_, i) => i % items.length)\n  );\n\n  const slots = SLOTS[bp];\n  const slotCount = SLOT_COUNTS[bp];\n\n  useEffect(() => {\n    const mqMobile = window.matchMedia(\"(max-width: 639px)\");\n    const mqTablet = window.matchMedia(\"(min-width: 640px) and (max-width: 1023px)\");\n    const update = () => {\n      if (mqMobile.matches) setBp(\"mobile\");\n      else if (mqTablet.matches) setBp(\"tablet\");\n      else setBp(\"desktop\");\n    };\n    update();\n    mqMobile.addEventListener(\"change\", update);\n    mqTablet.addEventListener(\"change\", update);\n    return () => {\n      mqMobile.removeEventListener(\"change\", update);\n      mqTablet.removeEventListener(\"change\", update);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!gridRef.current) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        isOffScreen.current = !entry.isIntersecting;\n      },\n      { threshold: 0.1 }\n    );\n    observer.observe(gridRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  useIsomorphicLayoutEffect(() => {\n    if (!flipStateRef.current || !hasEntered.current) return;\n    const state = flipStateRef.current;\n    flipStateRef.current = null;\n\n    flipTweenRef.current?.kill();\n    isAnimating.current = true;\n\n    flipTweenRef.current = Flip.from(state, {\n      duration: 1.0,\n      ease: \"power2.inOut\",\n      absolute: true,\n      zIndex: 20,\n      onComplete: () => {\n        isAnimating.current = false;\n      },\n    });\n  }, [assignment]);\n\n  const swapWithHero = (slotIdx: number) => {\n    if (slotIdx === CENTER_SLOT || isAnimating.current || !gridRef.current) return;\n    flipStateRef.current = Flip.getState(gridRef.current.querySelectorAll(DATA_SELECTOR));\n    setAssignment((prev) => {\n      const next = [...prev];\n      [next[CENTER_SLOT], next[slotIdx]] = [next[slotIdx], next[CENTER_SLOT]];\n      return next;\n    });\n  };\n\n  useEffect(() => {\n    if (!autoPlay) return;\n    const id = setInterval(() => {\n      if (isHovered.current || isOffScreen.current || isAnimating.current) return;\n      if (!gridRef.current) return;\n\n      const p = pickPeripheral(slotCount);\n      swapWithHero(p);\n    }, swapInterval);\n    return () => clearInterval(id);\n  }, [autoPlay, swapInterval, slotCount]);\n\n  useEffect(() => {\n    if (hasEntered.current || !gridRef.current) return;\n    hasEntered.current = true;\n    gsap.fromTo(\n      gridRef.current.querySelectorAll(DATA_SELECTOR),\n      { opacity: 0, y: 24, scale: 0.94 },\n      {\n        opacity: 1,\n        y: 0,\n        scale: 1,\n        duration: 0.6,\n        stagger: 0.05,\n        ease: \"power3.out\",\n        clearProps: \"opacity,transform\",\n      }\n    );\n  }, []);\n\n  useEffect(() => () => {\n    flipTweenRef.current?.kill();\n  }, []);\n\n  return (\n    <div\n      ref={gridRef}\n      onPointerEnter={() => {\n        isHovered.current = true;\n      }}\n      onPointerLeave={() => {\n        isHovered.current = false;\n      }}\n      role=\"region\"\n      aria-label=\"Moving bento grid\"\n      style={{\n        display: \"grid\",\n        gridTemplateColumns: `repeat(${GRID_COLS[bp]}, 1fr)`,\n        gridTemplateRows: \"repeat(3, 1fr)\",\n        gap: `${gap}px`,\n        aspectRatio: ASPECT_RATIOS[bp],\n        ...accentStyle(color),\n        ...style,\n      }}\n      className={cn(\"relative w-full select-none\", className)}\n      {...props}\n    >\n      {assignment.slice(0, slotCount).map((itemIdx, slotIdx) => {\n        const item = items[itemIdx] ?? items[itemIdx % items.length];\n        const isHero = slotIdx === CENTER_SLOT;\n        return (\n          <div\n            key={item.id}\n            {...{ [DATA_ATTR]: item.id }}\n            style={{\n              gridColumn: slots[slotIdx].gridColumn,\n              gridRow: slots[slotIdx].gridRow,\n            }}\n            className=\"size-full overflow-hidden\"\n          >\n            <BentoCard\n              item={item}\n              isHero={isHero}\n              onClick={() => {\n                if (!isHero) {\n                  swapWithHero(slotIdx);\n                  onCardClick?.(item, slotIdx);\n                }\n              }}\n            />\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n// ------------------------------------------\n// Circular Reel Standalone Component\n// ------------------------------------------\nexport const CircularReel = forwardRef<HTMLDivElement, CircularReelProps>(\n  (\n    {\n      items = DEFAULT_CIRCULAR_REEL_ITEMS,\n      color,\n      accentColor,\n      backgroundColor,\n      idleRotationSpeed = 5,\n      scrollSensitivity = 0.06,\n      ringThickness = 37,\n      fontSize = 12,\n      grayscale = false,\n      autoPlay = true,\n      onItemChange,\n      className,\n      style,\n      ...props\n    },\n    ref,\n  ) => {\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const [dimensions, setDimensions] = useState({ width: 800, height: 600 });\n    const [activeIndex, setActiveIndex] = useState(0);\n    const uid = React.useId().replace(/:/g, \"-\");\n\n    const spinnerGroupRef = useRef<SVGGElement | null>(null);\n    const indicatorGroupRef = useRef<SVGGElement | null>(null);\n\n    const isDragging = useRef(false);\n    const dragCenter = useRef({ x: 0, y: 0, lastAngle: 0 });\n\n    const rotRef = useRef({\n      currentIndicatorRotation: 0,\n      targetIndicatorRotation: 0,\n      currentSpinnerRotation: 0,\n      targetSpinnerRotation: 0,\n      direction: 1,\n      lastTime: typeof performance !== \"undefined\" ? performance.now() : 0,\n      lastIndex: 0,\n      rafId: 0,\n    });\n\n    const itemsRef = useRef(items);\n    itemsRef.current = items;\n    const onItemChangeRef = useRef(onItemChange);\n    onItemChangeRef.current = onItemChange;\n\n    useEffect(() => {\n      const el = containerRef.current;\n      if (!el) return;\n      const updateSize = () => {\n        const w = el.clientWidth || 800;\n        const h = el.clientHeight || Math.max(500, Math.min(w * 0.72, 680));\n        setDimensions({ width: w, height: h });\n      };\n      updateSize();\n      const observer = new ResizeObserver(updateSize);\n      observer.observe(el);\n      return () => observer.disconnect();\n    }, []);\n\n    const outerRadius = Math.max(120, Math.min(dimensions.width, dimensions.height) * 0.46);\n    const thicknessPct = Math.max(10, Math.min(60, ringThickness));\n    const innerRadius = outerRadius * (1 - thicknessPct / 100);\n    const centerX = dimensions.width / 2;\n    const centerY = dimensions.height / 2;\n\n    // Animation frame loop: uniform constant-speed auto-spin + frame-rate-independent scroll smoothing\n    useEffect(() => {\n      const lerp = (start: number, target: number, factor: number) =>\n        start + (target - start) * factor;\n\n      const tick = (now: number) => {\n        const state = rotRef.current;\n        let dt = (now - state.lastTime) / 1000;\n        state.lastTime = now;\n        if (dt <= 0 || dt > 0.1) dt = 1 / 60;\n\n        if (!isDragging.current && autoPlay) {\n          // Advance both current and target linearly so auto-scroll is 100% constant speed (never speeds up or slows down)\n          const autoDelta = idleRotationSpeed * 0.45 * state.direction * dt;\n          state.targetIndicatorRotation += autoDelta;\n          state.currentIndicatorRotation += autoDelta;\n          state.targetSpinnerRotation -= autoDelta;\n          state.currentSpinnerRotation -= autoDelta;\n        }\n\n        // Frame-rate-independent smoothing for user scroll / drag / click offsets\n        const smoothFactor = 1 - Math.exp(-8 * dt);\n        state.currentIndicatorRotation = lerp(\n          state.currentIndicatorRotation,\n          state.targetIndicatorRotation,\n          smoothFactor,\n        );\n        state.currentSpinnerRotation = lerp(\n          state.currentSpinnerRotation,\n          state.targetSpinnerRotation,\n          smoothFactor,\n        );\n\n        if (indicatorGroupRef.current) {\n          const indAngle = state.currentIndicatorRotation % 360;\n          indicatorGroupRef.current.setAttribute(\n            \"transform\",\n            `rotate(${indAngle} ${centerX} ${centerY})`,\n          );\n        }\n\n        if (spinnerGroupRef.current) {\n          const spinAngle = state.currentSpinnerRotation % 360;\n          spinnerGroupRef.current.setAttribute(\n            \"transform\",\n            `rotate(${spinAngle} ${centerX} ${centerY})`,\n          );\n        }\n\n        const total = itemsRef.current.length;\n        if (total > 0) {\n          const angleStep = 360 / total;\n          const diff =\n            ((state.currentIndicatorRotation - state.currentSpinnerRotation) % 360 + 360) % 360;\n          const idx = Math.floor(diff / angleStep) % total;\n          if (idx !== state.lastIndex) {\n            state.lastIndex = idx;\n            setActiveIndex(idx);\n            onItemChangeRef.current?.(itemsRef.current[idx], idx);\n          }\n        }\n\n        state.rafId = requestAnimationFrame(tick);\n      };\n\n      rotRef.current.lastTime = performance.now();\n      rotRef.current.rafId = requestAnimationFrame(tick);\n      return () => cancelAnimationFrame(rotRef.current.rafId);\n    }, [autoPlay, idleRotationSpeed, centerX, centerY]);\n\n    // Wheel & Window Scroll listeners with speed-dependent rotation boost\n    useEffect(() => {\n      const el = containerRef.current;\n      if (!el) return;\n\n      let lastWheelTime = performance.now();\n\n      const onWheel = (e: WheelEvent) => {\n        e.preventDefault();\n        const now = performance.now();\n        const elapsed = Math.max(8, now - lastWheelTime);\n        lastWheelTime = now;\n\n        const rawDelta = e.deltaY;\n        if (Math.abs(rawDelta) < 0.1) return;\n\n        const dir = rawDelta >= 0 ? 1 : -1;\n        rotRef.current.direction = dir;\n\n        // Calculate scroll speed (px/ms) so faster scrolling spins significantly more\n        const wheelSpeed = Math.abs(rawDelta) / elapsed;\n        const speedMultiplier = 1 + Math.min(wheelSpeed * 1.1 + Math.abs(rawDelta) / 60, 5);\n        const delta = rawDelta * scrollSensitivity * speedMultiplier;\n\n        rotRef.current.targetIndicatorRotation += delta;\n        rotRef.current.targetSpinnerRotation -= delta;\n      };\n\n      let lastScrollY = typeof window !== \"undefined\" ? window.scrollY : 0;\n      let lastScrollTime = performance.now();\n\n      const onWindowScroll = () => {\n        const now = performance.now();\n        const dy = window.scrollY - lastScrollY;\n        const elapsed = Math.max(8, now - lastScrollTime);\n        lastScrollY = window.scrollY;\n        lastScrollTime = now;\n\n        if (Math.abs(dy) < 1) return;\n        const dir = dy >= 0 ? 1 : -1;\n        rotRef.current.direction = dir;\n\n        const pageScrollSpeed = Math.abs(dy) / elapsed;\n        const speedMultiplier = 1 + Math.min(pageScrollSpeed * 1.5, 4.5);\n        const delta = dy * scrollSensitivity * 0.8 * speedMultiplier;\n\n        rotRef.current.targetIndicatorRotation += delta;\n        rotRef.current.targetSpinnerRotation -= delta;\n      };\n\n      el.addEventListener(\"wheel\", onWheel, { passive: false });\n      window.addEventListener(\"scroll\", onWindowScroll, { passive: true });\n      return () => {\n        el.removeEventListener(\"wheel\", onWheel);\n        window.removeEventListener(\"scroll\", onWindowScroll);\n      };\n    }, [scrollSensitivity]);\n\n    // Pointer Drag handlers\n    const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      isDragging.current = true;\n      (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n      const rect = containerRef.current?.getBoundingClientRect();\n      if (!rect) return;\n      const cx = rect.left + rect.width / 2;\n      const cy = rect.top + rect.height / 2;\n      const angle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);\n      dragCenter.current = { x: cx, y: cy, lastAngle: angle };\n    };\n\n    const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (!isDragging.current) return;\n      const { x: cx, y: cy, lastAngle } = dragCenter.current;\n      const angle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);\n      let delta = angle - lastAngle;\n      if (delta > 180) delta -= 360;\n      if (delta < -180) delta += 360;\n      rotRef.current.targetSpinnerRotation += delta;\n      rotRef.current.targetIndicatorRotation -= delta * 0.25;\n      dragCenter.current.lastAngle = angle;\n    };\n\n    const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n      isDragging.current = false;\n      try {\n        (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);\n      } catch {}\n    };\n\n    const handleSegmentClick = (index: number) => {\n      const total = items.length;\n      if (total === 0) return;\n      const angleStep = 360 / total;\n      const targetAngle = (index + 0.5) * angleStep;\n      rotRef.current.targetSpinnerRotation =\n        rotRef.current.currentIndicatorRotation - targetAngle;\n    };\n\n    const count = items.length;\n    const d = (2 * Math.PI) / (count || 1);\n\n    const segments = useMemo(() => {\n      return items.map((item, i) => {\n        const startAngle = i * d - Math.PI / 2;\n        const endAngle = (i + 1) * d - Math.PI / 2;\n        const midAngle = (startAngle + endAngle) / 2;\n\n        const x1 = centerX + outerRadius * Math.cos(startAngle);\n        const y1 = centerY + outerRadius * Math.sin(startAngle);\n        const x2 = centerX + outerRadius * Math.cos(endAngle);\n        const y2 = centerY + outerRadius * Math.sin(endAngle);\n        const x3 = centerX + innerRadius * Math.cos(endAngle);\n        const y3 = centerY + innerRadius * Math.sin(endAngle);\n        const x4 = centerX + innerRadius * Math.cos(startAngle);\n        const y4 = centerY + innerRadius * Math.sin(startAngle);\n\n        const pathD = `M ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 0 1 ${x2} ${y2} L ${x3} ${y3} A ${innerRadius} ${innerRadius} 0 0 0 ${x4} ${y4} Z`;\n\n        const midRadius = (innerRadius + outerRadius) / 2;\n        const midX = centerX + Math.cos(midAngle) * midRadius;\n        const midY = centerY + Math.sin(midAngle) * midRadius;\n        const imgW = outerRadius * d * 1.38;\n        const imgH = (outerRadius - innerRadius) * 1.38;\n        const imgRot = (midAngle * 180) / Math.PI + 90;\n\n        return {\n          item,\n          index: i,\n          pathD,\n          midX,\n          midY,\n          imgW,\n          imgH,\n          imgRot,\n        };\n      });\n    }, [items, d, centerX, centerY, outerRadius, innerRadius]);\n\n    const activeItem = items[activeIndex] ?? items[0];\n\n    return (\n      <div\n        ref={(node) => {\n          containerRef.current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n        }}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        onPointerLeave={() => {\n          isDragging.current = false;\n        }}\n        style={{\n          ...accentStyle(color ?? accentColor),\n          ...(backgroundColor ? { backgroundColor } : {}),\n          ...style,\n        }}\n        className={cn(\n          \"relative flex h-[400px] sm:h-[600px] md:h-[660px] lg:h-[720px] w-full select-none cursor-grab active:cursor-grabbing items-center justify-center overflow-hidden rounded-2xl\",\n          className,\n        )}\n        {...props}\n      >\n        {/* Full-Bleed Active Image Background (Instant Switch, No Blank Screen on Fast Scroll) */}\n        <div className=\"pointer-events-none absolute inset-0 overflow-hidden\" aria-hidden>\n          {items.map((item, idx) => (\n            <div\n              key={item.id ?? item.name + idx}\n              className={cn(\n                \"absolute inset-0\",\n                idx === activeIndex\n                  ? \"opacity-100\"\n                  : \"opacity-0 pointer-events-none\",\n              )}\n              style={{\n                filter: grayscale ? \"saturate(0)\" : undefined,\n              }}\n            >\n              <img\n                src={item.image}\n                alt={item.name}\n                className=\"size-full object-cover object-center\"\n              />\n            </div>\n          ))}\n        </div>\n\n        {/* SVG Segmented Wheel */}\n        <svg\n          className=\"absolute inset-0 size-full pointer-events-auto\"\n          viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}\n          aria-label=\"Circular reel spinner\"\n        >\n          <defs>\n            {segments.map((seg) => (\n              <clipPath key={`clip-${uid}-${seg.index}`} id={`reel-clip-${uid}-${seg.index}`}>\n                <path d={seg.pathD} />\n              </clipPath>\n            ))}\n          </defs>\n\n          {/* Rotating Spinner Group */}\n          <g ref={spinnerGroupRef}>\n            {segments.map((seg) => {\n              const isSelected = seg.index === activeIndex;\n              return (\n                <g\n                  key={seg.index}\n                  clipPath={`url(#reel-clip-${uid}-${seg.index})`}\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    handleSegmentClick(seg.index);\n                  }}\n                  className=\"cursor-pointer\"\n                >\n                  <image\n                    href={seg.item.image}\n                    width={seg.imgW}\n                    height={seg.imgH}\n                    x={seg.midX - seg.imgW / 2}\n                    y={seg.midY - seg.imgH / 2}\n                    preserveAspectRatio=\"xMidYMid slice\"\n                    transform={`rotate(${seg.imgRot} ${seg.midX} ${seg.midY})`}\n                    style={{\n                      filter: grayscale ? \"saturate(0)\" : undefined,\n                    }}\n                  />\n                  {/* Segment separation line */}\n                  <path\n                    d={seg.pathD}\n                    fill=\"none\"\n                    stroke={isSelected ? \"var(--hero-accent-text, #ffff2b)\" : \"rgba(255,255,255,0.18)\"}\n                    strokeWidth={isSelected ? \"2\" : \"0.75\"}\n                  />\n                </g>\n              );\n            })}\n          </g>\n\n          {/* Inner Lens Reticle Ring */}\n          <circle\n            cx={centerX}\n            cy={centerY}\n            r={Math.max(40, innerRadius * 0.91)}\n            fill=\"none\"\n            stroke=\"rgba(255,255,255,0.22)\"\n            strokeWidth=\"1\"\n            strokeDasharray=\"4 6\"\n            className=\"pointer-events-none\"\n          />\n\n          {/* Rotating Dial Indicator */}\n          <g ref={indicatorGroupRef} className=\"pointer-events-none\">\n            <line\n              x1={centerX}\n              y1={centerY - innerRadius * 0.85}\n              x2={centerX}\n              y2={centerY - outerRadius * 1.08}\n              stroke=\"var(--hero-accent-text, #ffff2b)\"\n              strokeWidth=\"3\"\n              strokeLinecap=\"round\"\n            />\n          </g>\n        </svg>\n\n        {/* Center Active Showcase HUD (Responsive Icons + Text) */}\n        <div\n          className=\"pointer-events-none absolute left-1/2 top-1/2 z-20 flex w-full -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center px-2 sm:px-3\"\n          style={{\n            maxWidth: `${Math.min(300, Math.max(136, Math.round(innerRadius * 1.56)))}px`,\n          }}\n        >\n          {/* Category & Index Pill */}\n          <div className=\"mb-1 sm:mb-2.5 inline-flex max-w-full items-center gap-1 sm:gap-1.5 rounded-full border border-white/25 bg-black/45 px-2 py-0.5 sm:px-3 sm:py-1 font-mono text-[8px] sm:text-[11px] uppercase tracking-wider sm:tracking-widest text-white shadow-lg backdrop-blur-md\">\n            <Compass\n              className=\"size-2.5 sm:size-3.5 shrink-0\"\n              style={{ color: \"var(--hero-accent-text, #ffff2b)\" }}\n            />\n            <span className=\"truncate max-w-[78px] xs:max-w-[100px] sm:max-w-[170px]\">\n              {activeItem?.category || \"Featured Scene\"}\n            </span>\n            <span className=\"opacity-40\">•</span>\n            <span className=\"tabular-nums opacity-85 shrink-0\">\n              {String(activeIndex + 1).padStart(2, \"0\")}/{String(items.length).padStart(2, \"0\")}\n            </span>\n          </div>\n\n          {/* Main Active Title Badge with Icons */}\n          <div\n            className=\"inline-flex max-w-full items-center justify-center gap-1 sm:gap-2 rounded-md px-2 py-1 sm:px-3.5 sm:py-1.5 font-mono text-[10px] sm:text-xs font-semibold uppercase tracking-wider shadow-xl whitespace-nowrap\"\n            style={{\n              backgroundColor: \"var(--hero-accent-text, #ffff2b)\",\n              color: \"#000000\",\n              fontSize: innerRadius < 130 ? \"10px\" : `${fontSize}px`,\n            }}\n          >\n            <Sparkles className=\"size-3 sm:size-4 shrink-0\" />\n            <span className=\"truncate\">{activeItem?.name || \"Widget\"}</span>\n            <span className=\"inline-flex size-3.5 sm:size-5 shrink-0 items-center justify-center rounded-sm bg-black/15\">\n              <ArrowUpRight className=\"size-2.5 sm:size-3.5\" />\n            </span>\n          </div>\n\n          {/* Scene Description */}\n          {activeItem?.description && innerRadius >= 85 && (\n            <p className=\"mt-1 sm:mt-2.5 line-clamp-1 sm:line-clamp-2 w-full rounded-md sm:rounded-lg border border-white/15 bg-black/40 px-2 py-1 sm:px-3 sm:py-1.5 text-[9px] sm:text-xs font-medium leading-tight sm:leading-relaxed text-white/95 shadow-md backdrop-blur-md\">\n              {activeItem.description}\n            </p>\n          )}\n\n          {/* Telemetry & Camera Metadata Row */}\n          <div className=\"mt-1 sm:mt-2.5 flex flex-wrap items-center justify-center gap-1 sm:gap-1.5\">\n            <span className=\"inline-flex items-center gap-1 rounded-full border border-white/20 bg-black/45 px-1.5 py-0.5 sm:px-2.5 font-mono text-[8px] sm:text-[10px] uppercase tracking-wider text-white/90 backdrop-blur-md\">\n              <Aperture\n                className=\"size-2.5 sm:size-3 shrink-0\"\n                style={{ color: \"var(--hero-accent-text, #ffff2b)\" }}\n              />\n              f/1.8\n            </span>\n            <span className=\"inline-flex items-center gap-1 rounded-full border border-white/20 bg-black/45 px-1.5 py-0.5 sm:px-2.5 font-mono text-[8px] sm:text-[10px] uppercase tracking-wider text-white/90 backdrop-blur-md\">\n              <Camera\n                className=\"size-2.5 sm:size-3 shrink-0\"\n                style={{ color: \"var(--hero-accent-text, #ffff2b)\" }}\n              />\n              {24 + (activeIndex % 4) * 12}mm\n            </span>\n            <span className=\"hidden sm:inline-flex items-center gap-1 rounded-full border border-white/20 bg-black/45 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-wider text-white/90 backdrop-blur-md\">\n              <MapPin\n                className=\"size-3 shrink-0\"\n                style={{ color: \"var(--hero-accent-text, #ffff2b)\" }}\n              />\n              {Math.round((activeIndex * 360) / Math.max(1, items.length))}°\n            </span>\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\n\nCircularReel.displayName = \"CircularReel\";\n\n// ------------------------------------------\n// 3D Pill Carousel Standalone Component\n// ------------------------------------------\nexport const ThreeDPillCarousel = forwardRef<HTMLDivElement, PillCarouselProps>(\n  (\n    {\n      items = DEFAULT_PILL_CAROUSEL_ITEMS,\n      color,\n      backgroundColor,\n      cardBorderRadius = 200,\n      cardShadow = \"0 20px 60px rgba(0, 0, 0, 0.3)\",\n      activeBorderWidth = 6,\n      activeBorderColor,\n      transitionDuration = 0.6,\n      hoverScale = 1.05,\n      hoverBrightness = 1.15,\n      autoSlide,\n      autoPlay = true,\n      autoPlayInterval = 3000,\n      tooltipText = \"View Details\",\n      tooltipBackgroundColor,\n      tooltipTextColor,\n      tooltipPadding = \"12px 24px\",\n      tooltipFontSize = 14,\n      tooltipBorderRadius = 50,\n      tooltipShadow = \"0 4px 12px rgba(0, 0, 0, 0.15)\",\n      linkNewTab = true,\n      onTooltipClick,\n      className,\n      style,\n      ...props\n    },\n    ref,\n  ) => {\n    const [currentIndex, setCurrentIndex] = useState(0);\n    const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n    const [tooltipHovered, setTooltipHovered] = useState(false);\n    const [tooltipPressed, setTooltipPressed] = useState(false);\n\n    const shouldAutoSlide = autoSlide ?? autoPlay ?? true;\n\n    const validItems = useMemo(\n      () =>\n        items.filter((item) => {\n          const src = typeof item?.image === \"string\" ? item.image : item?.image?.src;\n          return Boolean(src);\n        }),\n      [items],\n    );\n\n    const openLink = (url?: string) => {\n      if (!url || typeof window === \"undefined\") return;\n      if (linkNewTab) {\n        window.open(url, \"_blank\", \"noopener,noreferrer\");\n      } else {\n        window.location.href = url;\n      }\n    };\n\n    const handleNext = () => {\n      if (validItems.length === 0) return;\n      React.startTransition(() => {\n        setCurrentIndex((prev) => (prev + 1) % validItems.length);\n      });\n    };\n\n    const handlePrev = () => {\n      if (validItems.length === 0) return;\n      React.startTransition(() => {\n        setCurrentIndex((prev) => (prev - 1 + validItems.length) % validItems.length);\n      });\n    };\n\n    const handleCardClick = (index: number) => {\n      React.startTransition(() => {\n        setCurrentIndex(index);\n      });\n    };\n\n    useEffect(() => {\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n          e.preventDefault();\n          handleNext();\n        } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n          e.preventDefault();\n          handlePrev();\n        }\n      };\n      if (typeof window !== \"undefined\") {\n        window.addEventListener(\"keydown\", handleKeyDown);\n        return () => window.removeEventListener(\"keydown\", handleKeyDown);\n      }\n    }, [validItems.length]);\n\n    useEffect(() => {\n      if (!shouldAutoSlide || validItems.length <= 1) return;\n      const interval = setInterval(() => {\n        React.startTransition(() => {\n          setCurrentIndex((prev) => (prev + 1) % validItems.length);\n        });\n      }, autoPlayInterval);\n      return () => clearInterval(interval);\n    }, [shouldAutoSlide, autoPlayInterval, validItems.length]);\n\n    const getCardStyle = (index: number): React.CSSProperties => {\n      const total = validItems.length;\n      const diff = (index - currentIndex + total) % total;\n      const isHovered = hoveredIndex === index;\n\n      if (diff === 0) {\n        return {\n          transform: \"translateX(-50%) translateY(-50%) scale(1)\",\n          zIndex: 50,\n          opacity: 1,\n          left: \"50%\",\n          top: \"53%\",\n          filter: \"blur(0px) brightness(1)\",\n        };\n      }\n      if (diff === 1 || diff === -total + 1) {\n        return {\n          transform: `translateX(-50%) translateY(-50%) scale(${isHovered ? 0.85 * hoverScale : 0.85})`,\n          zIndex: 40,\n          opacity: isHovered ? 1 : 0.95,\n          left: \"88%\",\n          top: \"39%\",\n          filter: `blur(0.5px) brightness(${isHovered ? hoverBrightness : 1})`,\n        };\n      }\n      if (diff === 2 || diff === -total + 2) {\n        return {\n          transform: `translateX(-50%) translateY(-50%) scale(${isHovered ? 0.7 * hoverScale : 0.7})`,\n          zIndex: 30,\n          opacity: isHovered ? 0.9 : 0.85,\n          left: \"122%\",\n          top: \"27%\",\n          filter: `blur(1.5px) brightness(${isHovered ? hoverBrightness : 1})`,\n        };\n      }\n      if (diff === total - 1 || diff === -1) {\n        return {\n          transform: `translateX(-50%) translateY(-50%) scale(${isHovered ? 0.85 * hoverScale : 0.85})`,\n          zIndex: 40,\n          opacity: isHovered ? 1 : 0.95,\n          left: \"12%\",\n          top: \"64%\",\n          filter: `blur(0.5px) brightness(${isHovered ? hoverBrightness : 1})`,\n        };\n      }\n      if (diff === total - 2 || diff === -2) {\n        return {\n          transform: `translateX(-50%) translateY(-50%) scale(${isHovered ? 0.7 * hoverScale : 0.7})`,\n          zIndex: 30,\n          opacity: isHovered ? 0.9 : 0.85,\n          left: \"-22%\",\n          top: \"74%\",\n          filter: `blur(1.5px) brightness(${isHovered ? hoverBrightness : 1})`,\n        };\n      }\n      return {\n        transform: \"translateX(-50%) translateY(-50%) scale(0.5)\",\n        zIndex: 10,\n        opacity: 0,\n        left: \"50%\",\n        top: \"53%\",\n        filter: \"blur(3px) brightness(1)\",\n      };\n    };\n\n    const effectiveBorderColor = activeBorderColor || \"var(--hero-accent-text, var(--primary))\";\n    const effectiveTooltipBg = tooltipBackgroundColor || \"var(--hero-accent-text, var(--primary))\";\n    const effectiveTooltipText = tooltipTextColor || \"var(--primary-foreground)\";\n\n    if (validItems.length === 0) {\n      return (\n        <div\n          ref={ref}\n          style={{ ...style, ...(backgroundColor ? { backgroundColor } : {}) }}\n          className={cn(\n            \"relative flex h-[540px] sm:h-[640px] md:h-[700px] w-full items-center justify-center rounded-2xl bg-background text-foreground text-base transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          Add images to the carousel\n        </div>\n      );\n    }\n\n    const currentItem = validItems[currentIndex];\n\n    return (\n      <div\n        ref={ref}\n        style={{\n          ...accentStyle(color),\n          ...(backgroundColor ? { backgroundColor } : {}),\n          ...style,\n        }}\n        className={cn(\n          \"relative h-[420px] xs:h-[480px] sm:h-[580px] md:h-[680px] lg:h-[740px] w-full select-none overflow-hidden rounded-2xl bg-background text-foreground transition-colors duration-300\",\n          className,\n        )}\n        {...props}\n      >\n        <style\n          dangerouslySetInnerHTML={{\n            __html: `\n              @keyframes pillTooltipEnter {\n                from {\n                  opacity: 0;\n                  transform: translate(-50%, 20px) scale(0.8);\n                }\n                to {\n                  opacity: 1;\n                  transform: translate(-50%, 0px) scale(1);\n                }\n              }\n            `,\n          }}\n        />\n\n        {/* Center Pill Column Stage */}\n        <div className=\"absolute left-1/2 top-1/2 h-full w-[155px] xs:w-[195px] sm:w-[245px] md:w-[275px] lg:w-[290px] -translate-x-1/2 -translate-y-1/2\">\n          {validItems.map((item, index) => {\n            const cardStyle = getCardStyle(index);\n            const isCenter = (index - currentIndex + validItems.length) % validItems.length === 0;\n            const imgSrc = typeof item.image === \"string\" ? item.image : item.image?.src || \"\";\n            const imgAlt =\n              typeof item.image === \"string\"\n                ? item.tooltipText || `Carousel image ${index + 1}`\n                : item.image?.alt || item.tooltipText || \"Carousel image\";\n\n            return (\n              <div\n                key={item.id ?? index}\n                style={{\n                  position: \"absolute\",\n                  width: \"100%\",\n                  height: \"75%\",\n                  ...cardStyle,\n                  transition: `all ${transitionDuration}s cubic-bezier(0.4, 0, 0.2, 1)`,\n                  cursor: \"pointer\",\n                }}\n                onClick={() => {\n                  if (isCenter) {\n                    openLink(item.link);\n                  } else {\n                    handleCardClick(index);\n                  }\n                }}\n                onMouseEnter={() => !isCenter && setHoveredIndex(index)}\n                onMouseLeave={() => setHoveredIndex(null)}\n              >\n                <div\n                  style={{\n                    position: \"relative\",\n                    width: \"100%\",\n                    height: \"100%\",\n                    borderRadius: `${cardBorderRadius}px`,\n                    overflow: \"hidden\",\n                    boxShadow:\n                      hoveredIndex === index && !isCenter\n                        ? \"0 25px 70px rgba(0, 0, 0, 0.4)\"\n                        : cardShadow,\n                    transition: \"box-shadow 0.3s ease\",\n                    border: isCenter\n                      ? `${activeBorderWidth}px solid ${effectiveBorderColor}`\n                      : \"none\",\n                  }}\n                >\n                  <img\n                    src={imgSrc}\n                    alt={imgAlt}\n                    draggable={false}\n                    className=\"size-full object-cover\"\n                  />\n                </div>\n              </div>\n            );\n          })}\n        </div>\n\n        {/* Floating Tooltip CTA above Center Pill */}\n        {validItems.length > 0 && (\n          <button\n            key={currentIndex}\n            type=\"button\"\n            onClick={(e) => {\n              e.stopPropagation();\n              if (currentItem?.link) {\n                openLink(currentItem.link);\n              } else if (onTooltipClick && currentItem) {\n                onTooltipClick(currentItem, currentIndex);\n              } else {\n                handleNext();\n              }\n            }}\n            onMouseEnter={() => setTooltipHovered(true)}\n            onMouseLeave={() => {\n              setTooltipHovered(false);\n              setTooltipPressed(false);\n            }}\n            onMouseDown={() => setTooltipPressed(true)}\n            onMouseUp={() => setTooltipPressed(false)}\n            style={{\n              position: \"absolute\",\n              top: \"calc(15.5% - 52px)\",\n              left: \"50%\",\n              transform: `translate(-50%, 0px) scale(${tooltipPressed ? 0.95 : tooltipHovered ? 1.05 : 1})`,\n              backgroundColor: effectiveTooltipBg,\n              color: effectiveTooltipText,\n              padding: tooltipPadding,\n              borderRadius: `${tooltipBorderRadius}px`,\n              border: \"none\",\n              cursor: \"pointer\",\n              fontSize: `${tooltipFontSize}px`,\n              fontWeight: 600,\n              letterSpacing: \"-0.01em\",\n              boxShadow: tooltipHovered\n                ? \"0 8px 24px rgba(0, 0, 0, 0.25)\"\n                : tooltipShadow,\n              transition: \"box-shadow 0.3s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)\",\n              animation: \"pillTooltipEnter 0.4s cubic-bezier(0.4, 0, 0.2, 1)\",\n              zIndex: 100,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {currentItem?.tooltipText || tooltipText}\n            <div\n              style={{\n                position: \"absolute\",\n                bottom: \"-8px\",\n                left: \"50%\",\n                transform: \"translateX(-50%)\",\n                width: 0,\n                height: 0,\n                borderLeft: \"8px solid transparent\",\n                borderRight: \"8px solid transparent\",\n                borderTop: `8px solid ${effectiveTooltipBg}`,\n              }}\n            />\n          </button>\n        )}\n      </div>\n    );\n  },\n);\n\nThreeDPillCarousel.displayName = \"ThreeDPillCarousel\";\n\n// ------------------------------------------\n// Arc Carousel Gallery (Framer-inspired)\n// ------------------------------------------\nexport const ArcCarouselGallery = forwardRef<HTMLDivElement, ArcCarouselProps>(\n  (\n    {\n      items = DEFAULT_ARC_CAROUSEL_ITEMS,\n      color,\n      backgroundColor,\n      cardSize = 220,\n      cardHeight,\n      gap = 20,\n      curve = -200,\n      perspective = 1200,\n      autoScrollSpeed = 0.22,\n      autoSlide = true,\n      autoPlay = true,\n      dragSpeed = 1.2,\n      inertia = 0.75,\n      borderRadius = 20,\n      linkNewTab = true,\n      onCardClick,\n      className,\n      style,\n      ...props\n    },\n    ref,\n  ) => {\n    const rootRef = useRef<HTMLDivElement | null>(null);\n    const [containerSize, setContainerSize] = useState(() => ({\n      width: typeof window !== \"undefined\" ? Math.min(window.innerWidth, 1200) : 1000,\n      height: typeof window !== \"undefined\" ? Math.min(window.innerHeight, 600) : 600,\n    }));\n    const [isAutoPlaying, setIsAutoPlaying] = useState(autoSlide ?? autoPlay);\n    const [isHovered, setIsHovered] = useState(false);\n    const [isDragging, setIsDragging] = useState(false);\n    const [tick, setTick] = useState(0);\n\n    useEffect(() => {\n      setIsAutoPlaying(autoSlide ?? autoPlay);\n    }, [autoSlide, autoPlay]);\n\n    const physicsRef = useRef({\n      offset: 0,\n      velocity: autoSlide ?? autoPlay ? autoScrollSpeed : 0,\n      dragging: false,\n      pointerId: -1,\n      startX: 0,\n      lastX: 0,\n      lastTime: 0,\n      samples: [] as { x: number; t: number }[],\n    });\n\n    useEffect(() => {\n      if (!rootRef.current) return;\n      const updateSize = () => {\n        if (rootRef.current) {\n          const rect = rootRef.current.getBoundingClientRect();\n          if (rect.width > 0) {\n            setContainerSize({\n              width: rect.width,\n              height: rect.height,\n            });\n          }\n        }\n      };\n      updateSize();\n\n      if (typeof ResizeObserver === \"undefined\") return;\n      const ro = new ResizeObserver((entries) => {\n        const entry = entries[0];\n        if (entry?.contentRect && entry.contentRect.width > 0) {\n          setContainerSize({\n            width: entry.contentRect.width,\n            height: entry.contentRect.height,\n          });\n        }\n      });\n      ro.observe(rootRef.current);\n      return () => ro.disconnect();\n    }, []);\n\n    const validItems = useMemo(\n      () =>\n        (items || []).filter((item) => {\n          const src = typeof item?.image === \"string\" ? item.image : item?.image?.src;\n          return Boolean(src);\n        }),\n      [items],\n    );\n\n    const effectiveCardWidth = useMemo(() => {\n      // Very small mobile screens (< 380px)\n      if (containerSize.width < 380) {\n        return Math.max(92, Math.min(cardSize, Math.round(containerSize.width * 0.31)));\n      }\n      // Small mobile screens (380px - 480px)\n      if (containerSize.width < 480) {\n        return Math.max(108, Math.min(cardSize, Math.round(containerSize.width * 0.33)));\n      }\n      // Phablets / small tablets (480px - 768px)\n      if (containerSize.width < 768) {\n        return Math.max(138, Math.min(cardSize, Math.round(containerSize.width * 0.28)));\n      }\n      // Tablets / small desktop (768px - 1024px)\n      if (containerSize.width < 1024) {\n        return Math.max(168, Math.min(cardSize, Math.round(containerSize.width * 0.24)));\n      }\n      return cardSize;\n    }, [containerSize.width, cardSize]);\n\n    const effectiveGap = useMemo(() => {\n      if (containerSize.width < 380) return Math.min(gap, 8);\n      if (containerSize.width < 480) return Math.min(gap, 10);\n      if (containerSize.width < 768) return Math.min(gap, 12);\n      return gap;\n    }, [containerSize.width, gap]);\n\n    const effectiveCardHeight = useMemo(() => {\n      if (cardHeight) {\n        const ratio = effectiveCardWidth / cardSize;\n        return Math.round(cardHeight * ratio);\n      }\n      return Math.round(effectiveCardWidth * 1.34);\n    }, [cardHeight, effectiveCardWidth, cardSize]);\n\n    const effectiveBorderRadius = useMemo(() => {\n      if (containerSize.width < 480) return Math.min(borderRadius, 14);\n      if (containerSize.width < 768) return Math.min(borderRadius, 16);\n      return borderRadius;\n    }, [containerSize.width, borderRadius]);\n\n    const effectivePerspective = useMemo(() => {\n      if (containerSize.width < 420) return 480;\n      if (containerSize.width < 640) return 580;\n      if (containerSize.width < 768) return 720;\n      if (containerSize.width < 1024) return Math.min(perspective, 950);\n      return perspective;\n    }, [containerSize.width, perspective]);\n\n    const pitch = Math.max(1, effectiveCardWidth + effectiveGap);\n\n    const arcGeometry = useMemo(() => {\n      const curveStrength = Math.min(1, Math.abs(curve) / 300);\n      const sign = curve < 0 ? -1 : (curve > 0 ? 1 : 0);\n      const count = Math.max(10, Math.min(42, Math.ceil(containerSize.width / pitch) + 12));\n      const half = Math.floor(count / 2);\n\n      // Boost angle step on smaller screens so the minus curvature is clearly visible and proportional\n      const mobileBoost =\n        containerSize.width < 380 ? 2.35 :\n        containerSize.width < 480 ? 2.05 :\n        containerSize.width < 640 ? 1.65 :\n        containerSize.width < 768 ? 1.35 :\n        containerSize.width < 1024 ? 1.15 : 1.0;\n\n      const baseStep = curveStrength === 0 ? 0 : (0.045 + curveStrength * 0.18) * mobileBoost;\n      const angleStep = baseStep;\n      const radius = angleStep === 0 ? 999999 : pitch / angleStep;\n      const maxTheta = half * angleStep;\n      const edgeCos = Math.cos(maxTheta);\n\n      return {\n        curveStrength,\n        sign,\n        angleStep,\n        radius,\n        count,\n        half,\n        maxTheta,\n        edgeCos,\n      };\n    }, [curve, pitch, containerSize.width]);\n\n    const computeSampleVelocity = useCallback(() => {\n      const samples = physicsRef.current.samples;\n      if (samples.length < 2) return physicsRef.current.velocity;\n      const first = samples[0];\n      const last = samples[samples.length - 1];\n      const dt = (last.t - first.t) / 1000;\n      if (dt <= 0) return physicsRef.current.velocity;\n      return -((last.x - first.x) / dt * dragSpeed) / pitch * 0.95;\n    }, [dragSpeed, pitch]);\n\n    useEffect(() => {\n      if (typeof window === \"undefined\" || validItems.length === 0) return;\n      let animId: number;\n\n      const loop = (timestamp: number) => {\n        const p = physicsRef.current;\n        if (p.lastTime === 0) p.lastTime = timestamp;\n        const dt = Math.max(0.001, Math.min(0.05, (timestamp - p.lastTime) / 1000));\n        p.lastTime = timestamp;\n\n        if (!p.dragging) {\n          const decay = Math.pow(0.82 + Math.max(0, Math.min(1, inertia)) * 0.17, dt * 60);\n          p.velocity *= decay;\n\n          const targetVelocity = isAutoPlaying\n            ? isHovered\n              ? autoScrollSpeed * 0.35\n              : autoScrollSpeed\n            : 0;\n\n          const blend = 1 - Math.exp(-1.2 * dt);\n          p.velocity += (targetVelocity - p.velocity) * blend;\n          p.offset += p.velocity * dt;\n        }\n\n        setTick((prev) => (prev + 1) % 1000000);\n        animId = requestAnimationFrame(loop);\n      };\n\n      animId = requestAnimationFrame(loop);\n      return () => {\n        cancelAnimationFrame(animId);\n        physicsRef.current.lastTime = 0;\n      };\n    }, [validItems.length, inertia, isAutoPlaying, isHovered, autoScrollSpeed]);\n\n    const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (!rootRef.current) return;\n      const p = physicsRef.current;\n      p.dragging = true;\n      p.pointerId = e.pointerId;\n      p.startX = e.clientX;\n      p.lastX = e.clientX;\n      p.samples = [{ x: e.clientX, t: performance.now() }];\n      setIsDragging(true);\n      rootRef.current.setPointerCapture(e.pointerId);\n    };\n\n    const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      const p = physicsRef.current;\n      if (!p.dragging || p.pointerId !== e.pointerId) return;\n      const deltaX = e.clientX - p.lastX;\n      p.lastX = e.clientX;\n      p.offset += (-deltaX / pitch) * dragSpeed;\n\n      const now = performance.now();\n      p.samples.push({ x: e.clientX, t: now });\n      while (p.samples.length > 8) p.samples.shift();\n      while (p.samples.length > 2 && now - p.samples[0].t > 120) p.samples.shift();\n\n      p.velocity = computeSampleVelocity();\n    };\n\n    const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n      const p = physicsRef.current;\n      if (!p.dragging || p.pointerId !== e.pointerId) return;\n      p.dragging = false;\n      p.velocity = computeSampleVelocity();\n      p.samples = [];\n      setIsDragging(false);\n      if (rootRef.current && rootRef.current.hasPointerCapture(e.pointerId)) {\n        rootRef.current.releasePointerCapture(e.pointerId);\n      }\n    };\n\n    const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {\n      const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;\n      if (Math.abs(delta) < 2) return;\n      physicsRef.current.offset += (delta / pitch) * 0.15;\n      physicsRef.current.velocity += (delta / pitch) * 0.6;\n    };\n\n    const handleCardClick = (item: ArcCarouselItem, index: number, e: React.MouseEvent) => {\n      const p = physicsRef.current;\n      if (Math.abs(e.clientX - p.startX) > 8) return;\n      if (item.link) {\n        if (linkNewTab && typeof window !== \"undefined\") {\n          window.open(item.link, \"_blank\", \"noopener,noreferrer\");\n        } else if (typeof window !== \"undefined\") {\n          window.location.href = item.link;\n        }\n      }\n      onCardClick?.(item, index);\n    };\n\n    const cards = useMemo(() => {\n      if (validItems.length === 0) return [];\n      const { angleStep, radius, curveStrength, sign, count, half, edgeCos } = arcGeometry;\n      const offset = physicsRef.current.offset;\n      const startIndex = Math.floor(offset) - half;\n\n      return Array.from({ length: count }, (_, idx) => {\n        const slot = startIndex + idx;\n        const d = slot - offset;\n        const wrappedIdx = ((slot % validItems.length) + validItems.length) % validItems.length;\n        const item = validItems[wrappedIdx];\n\n        if (angleStep === 0 || curveStrength === 0) {\n          return {\n            key: `${idx}-${wrappedIdx}`,\n            item,\n            index: wrappedIdx,\n            x: d * pitch,\n            z: 0,\n            rotateY: 0,\n            opacity: 1,\n            zIndex: 1000 - Math.abs(Math.round(d)),\n          };\n        }\n\n        const m = d * angleStep;\n        const x = Math.sin(m) * radius;\n\n        // Minus curve when curve < 0 (e.g. -200):\n        // sign = -1 gives the true minus curve:\n        // Right side (d > 0): negative rotateY. Left side (d < 0): positive rotateY.\n        const z = (Math.cos(m) - (1 + edgeCos) / 2) * radius * sign;\n        const rotateY = ((m * 180) / Math.PI) * sign;\n\n        const fade = Math.min(1, Math.abs(d) / Math.max(1, half));\n        const opacity = 1 - fade * 0.18;\n        const zIndex = 1000 - Math.abs(Math.round(m * 100));\n\n        return {\n          key: `${idx}-${wrappedIdx}`,\n          item,\n          index: wrappedIdx,\n          x,\n          z,\n          rotateY,\n          opacity,\n          zIndex,\n        };\n      });\n    }, [validItems, arcGeometry, pitch, tick]);\n\n    if (validItems.length === 0) {\n      return (\n        <div\n          ref={ref}\n          style={{ ...style, ...(backgroundColor ? { backgroundColor } : {}) }}\n          className={cn(\n            \"relative flex h-[500px] sm:h-[580px] md:h-[640px] w-full items-center justify-center rounded-2xl bg-background text-foreground text-base transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          Add images to the arc carousel\n        </div>\n      );\n    }\n\n    return (\n      <div\n        ref={(node) => {\n          rootRef.current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n        }}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        onWheel={handleWheel}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n        style={{\n          ...accentStyle(color),\n          ...(backgroundColor ? { backgroundColor } : {}),\n          ...style,\n        }}\n        className={cn(\n          \"relative h-[340px] xs:h-[380px] sm:h-[480px] md:h-[580px] lg:h-[680px] w-full select-none overflow-hidden rounded-2xl bg-background text-foreground transition-colors duration-300 touch-none\",\n          isDragging ? \"cursor-grabbing\" : \"cursor-grab\",\n          className,\n        )}\n        role=\"region\"\n        aria-label=\"Arc Carousel Gallery\"\n        {...props}\n      >\n        {/* 3D Arc Perspective Stage */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            perspective: `${effectivePerspective}px`,\n            perspectiveOrigin: \"50% 50%\",\n            transformStyle: \"preserve-3d\",\n          }}\n        >\n          {cards.map((c) => {\n            const imgSrc = typeof c.item.image === \"string\" ? c.item.image : c.item.image?.src;\n            const imgAlt =\n              typeof c.item.image === \"string\"\n                ? c.item.title || \"Gallery card\"\n                : c.item.image?.alt || c.item.title || \"Gallery card\";\n\n            return (\n              <div\n                key={c.key}\n                style={{\n                  position: \"absolute\",\n                  left: \"50%\",\n                  top: \"50%\",\n                  width: effectiveCardWidth,\n                  height: effectiveCardHeight,\n                  borderRadius: `${effectiveBorderRadius}px`,\n                  overflow: \"hidden\",\n                  transformStyle: \"preserve-3d\",\n                  backfaceVisibility: \"hidden\",\n                  WebkitBackfaceVisibility: \"hidden\",\n                  opacity: c.opacity,\n                  zIndex: c.zIndex,\n                  transform: `translate3d(-50%, -50%, 0) translateX(${c.x}px) translateZ(${c.z}px) rotateY(${c.rotateY}deg)`,\n                  willChange: \"transform, opacity\",\n                  boxShadow: \"0 22px 55px rgba(0, 0, 0, 0.28)\",\n                }}\n                onClick={(e) => handleCardClick(c.item, c.index, e)}\n                className=\"group select-none border border-border/20 transition-shadow duration-300 hover:shadow-2xl hover:border-foreground/20\"\n              >\n                <img\n                  src={imgSrc}\n                  alt={imgAlt}\n                  draggable={false}\n                  className=\"size-full object-cover pointer-events-none transition-transform duration-500 group-hover:scale-105\"\n                />\n                {(c.item.title || c.item.subtitle) && (\n                  <div className=\"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent p-2 sm:p-3.5 pt-5 sm:pt-8 text-white pointer-events-none\">\n                    {c.item.title && <p className=\"text-[11px] sm:text-xs font-bold leading-tight drop-shadow truncate\">{c.item.title}</p>}\n                    {c.item.subtitle && <p className=\"text-[9px] sm:text-[10px] text-white/80 drop-shadow leading-tight mt-0.5 truncate\">{c.item.subtitle}</p>}\n                  </div>\n                )}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    );\n  },\n);\n\nArcCarouselGallery.displayName = \"ArcCarouselGallery\";\n\n// ------------------------------------------\n// 3D VIDEO SPHERE GALLERY COMPONENT\n// Inspired by Framer Video Sphere Gallery\n// ------------------------------------------\n\nexport const VideoSphereGallery = forwardRef<HTMLDivElement, VideoSphereGalleryProps>(\n  (\n    {\n      items = DEFAULT_VIDEO_SPHERE_ITEMS,\n      radius = 280,\n      itemCount = 35,\n      autoRotateSpeed = 0.0015,\n      itemWidth = 110,\n      itemHeight = 150,\n      perspective = 1200,\n      blurStrength = 12,\n      autoRotate = true,\n      muted = true,\n      loop = true,\n      unmuteOnHover = false,\n      maxConcurrentVideos = 12,\n      hoverScale = 1.1,\n      enableHoverScale = true,\n      enablePopup = true,\n      popupWidth = 400,\n      popupHeight = 500,\n      cardBorderColor,\n      color,\n      className,\n      style,\n      onCardClick,\n      ...props\n    },\n    ref,\n  ) => {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n    const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);\n\n    const [mounted, setMounted] = useState(false);\n    useEffect(() => {\n      setMounted(true);\n    }, []);\n\n    const [selectedItem, setSelectedItem] = useState<VideoSphereItem | null>(null);\n    const selectedItemRef = useRef<VideoSphereItem | null>(null);\n    selectedItemRef.current = selectedItem;\n\n    const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n    const hoveredIndexRef = useRef<number | null>(null);\n    hoveredIndexRef.current = hoveredIndex;\n\n    const [isGrabbing, setIsGrabbing] = useState(false);\n    const isDragging = useRef(false);\n    const hasMoved = useRef(false);\n    const dragStartPos = useRef({ x: 0, y: 0 });\n    const lastMousePos = useRef({ x: 0, y: 0 });\n    const velocityX = useRef(0);\n    const velocityY = useRef(0);\n    const cardPressRef = useRef<{ item: VideoSphereItem; idx: number; x: number; y: number; time: number } | null>(null);\n    const capturedPointerId = useRef<number | null>(null);\n\n    const rotX = useRef(0);\n    const rotY = useRef(0);\n    const currentRotX = useRef(0);\n    const currentRotY = useRef(0);\n    const isVisibleRef = useRef(true);\n    const lastCheckTime = useRef(0);\n\n    // Responsive sizing using ResizeObserver & window resize fallback\n    const [responsiveRadius, setResponsiveRadius] = useState(radius);\n    const [responsiveWidth, setResponsiveWidth] = useState(itemWidth);\n    const [responsiveHeight, setResponsiveHeight] = useState(itemHeight);\n\n    useEffect(() => {\n      const el = containerRef.current;\n      if (!el) return;\n\n      const updateSize = () => {\n        const w = el.clientWidth || (typeof window !== \"undefined\" ? window.innerWidth : 1200);\n        if (w < 380) {\n          // Extra small phone (320px - 379px)\n          setResponsiveRadius(Math.min(radius * 0.52, 145));\n          setResponsiveWidth(Math.min(itemWidth * 0.65, 72));\n          setResponsiveHeight(Math.min(itemHeight * 0.65, 98));\n        } else if (w < 500) {\n          // Small phone (380px - 499px)\n          setResponsiveRadius(Math.min(radius * 0.62, 172));\n          setResponsiveWidth(Math.min(itemWidth * 0.74, 82));\n          setResponsiveHeight(Math.min(itemHeight * 0.74, 112));\n        } else if (w < 768) {\n          // Large mobile / small tablet (500px - 767px)\n          setResponsiveRadius(Math.min(radius * 0.75, 210));\n          setResponsiveWidth(Math.min(itemWidth * 0.85, 94));\n          setResponsiveHeight(Math.min(itemHeight * 0.85, 128));\n        } else if (w < 1024) {\n          // Tablet (768px - 1023px)\n          setResponsiveRadius(Math.min(radius * 0.88, 245));\n          setResponsiveWidth(Math.min(itemWidth * 0.92, 102));\n          setResponsiveHeight(Math.min(itemHeight * 0.92, 138));\n        } else {\n          // Desktop (1024px+)\n          setResponsiveRadius(radius);\n          setResponsiveWidth(itemWidth);\n          setResponsiveHeight(itemHeight);\n        }\n      };\n\n      updateSize();\n\n      if (typeof ResizeObserver !== \"undefined\") {\n        const ro = new ResizeObserver(() => updateSize());\n        ro.observe(el);\n        return () => ro.disconnect();\n      }\n\n      window.addEventListener(\"resize\", updateSize);\n      return () => window.removeEventListener(\"resize\", updateSize);\n    }, [radius, itemWidth, itemHeight]);\n\n    // Proportionally scale 3D perspective to preserve field-of-view on all viewports\n    const responsivePerspective = useMemo(() => {\n      return Math.round(perspective * (responsiveRadius / radius));\n    }, [perspective, responsiveRadius, radius]);\n\n    // Populate display items up to itemCount\n    const displayItems = useMemo(() => {\n      const valid = (items || []).filter((i) => i && i.src);\n      const source = valid.length > 0 ? valid : DEFAULT_VIDEO_SPHERE_ITEMS;\n      const count = Math.max(itemCount, source.length);\n      const result: VideoSphereItem[] = [];\n      for (let i = 0; i < count; i++) {\n        result.push(source[i % source.length]);\n      }\n      return result;\n    }, [items, itemCount]);\n\n    // Calculate spherical distribution coordinates (Fibonacci spiral sphere)\n    const positions = useMemo(() => {\n      const result: { x: number; y: number; z: number }[] = [];\n      const total = displayItems.length;\n      const goldenAngle = Math.PI * (3 - Math.sqrt(5));\n      for (let i = 0; i < total; i++) {\n        const yRatio = total > 1 ? 1 - (i / (total - 1)) * 2 : 0;\n        const radiusAtY = Math.sqrt(Math.max(0, 1 - yRatio * yRatio));\n        const theta = goldenAngle * i;\n        result.push({\n          x: Math.cos(theta) * radiusAtY * responsiveRadius,\n          y: yRatio * responsiveRadius,\n          z: Math.sin(theta) * radiusAtY * responsiveRadius,\n        });\n      }\n      return result;\n    }, [displayItems.length, responsiveRadius]);\n\n    // Viewport intersection observer to pause when offscreen\n    useEffect(() => {\n      const el = containerRef.current;\n      if (!el || typeof IntersectionObserver === \"undefined\") return;\n      const obs = new IntersectionObserver(\n        ([e]) => {\n          isVisibleRef.current = e.isIntersecting;\n        },\n        { threshold: 0.05 },\n      );\n      obs.observe(el);\n      return () => obs.disconnect();\n    }, []);\n\n    // 60 FPS Animation & 3D projection loop\n    useEffect(() => {\n      let animId: number;\n\n      const frame = (time: number) => {\n        // Auto-rotation when not interacting or modal open\n        if (autoRotate && !isDragging.current && hoveredIndexRef.current === null && !selectedItemRef.current) {\n          rotY.current += autoRotateSpeed;\n        }\n\n        // Smooth spring-like lerp towards target rotation angles\n        currentRotX.current += (rotX.current - currentRotX.current) * 0.08;\n        currentRotY.current += (rotY.current - currentRotY.current) * 0.08;\n\n        const cosX = Math.cos(currentRotX.current);\n        const sinX = Math.sin(currentRotX.current);\n        const cosY = Math.cos(currentRotY.current);\n        const sinY = Math.sin(currentRotY.current);\n\n        const posLen = positions.length;\n        const depthList: { idx: number; z: number }[] = [];\n\n        for (let i = 0; i < posLen; i++) {\n          const p = positions[i];\n          const el = cardRefs.current[i];\n          if (!el) continue;\n\n          // 3D camera projection matching Framer\n          const px = p.x * cosY + p.z * sinY;\n          const py = p.y * cosX + (p.z * cosY - p.x * sinY) * sinX;\n          const pz = (p.z * cosY - p.x * sinY) * cosX - p.y * sinX;\n\n          depthList.push({ idx: i, z: pz });\n\n          const isHov = hoveredIndexRef.current === i;\n          const depth = Math.max(0, (responsiveRadius - pz) / (responsiveRadius * 2));\n          const baseScale = Math.max(0.45, Math.min(1.2, 0.5 + ((pz + responsiveRadius) / (responsiveRadius * 2)) * 0.7));\n          const scale = isHov ? (enableHoverScale ? baseScale * hoverScale : baseScale) : baseScale;\n          const opacity = isHov ? 1 : Math.max(0.1, Math.min(1, 0.1 + ((pz + responsiveRadius) / (responsiveRadius * 2)) * 0.9));\n          const zIndex = isHov ? 2000 : Math.round(pz + responsiveRadius);\n          const blur = isHov ? 0 : depth * blurStrength;\n          const gray = isHov ? 0 : depth * 100;\n\n          el.style.transform = `translate3d(calc(-50% + ${px.toFixed(1)}px), calc(-50% + ${py.toFixed(1)}px), ${pz.toFixed(1)}px) scale(${scale.toFixed(3)})`;\n          el.style.opacity = `${opacity.toFixed(2)}`;\n          el.style.zIndex = `${zIndex}`;\n          el.style.filter = `blur(${blur.toFixed(1)}px) grayscale(${gray.toFixed(0)}%)`;\n          el.style.pointerEvents = pz > -responsiveRadius * 0.35 ? \"auto\" : \"none\";\n        }\n\n        // Playback optimization: throttle check every 150ms\n        if (time - lastCheckTime.current > 150) {\n          lastCheckTime.current = time;\n          if (isVisibleRef.current) {\n            depthList.sort((a, b) => b.z - a.z);\n            const playSet = new Set<number>();\n            const limit = Math.min(maxConcurrentVideos, depthList.length);\n            for (let j = 0; j < limit; j++) {\n              if (depthList[j].z > -responsiveRadius * 0.25) {\n                playSet.add(depthList[j].idx);\n              }\n            }\n            videoRefs.current.forEach((v, idx) => {\n              if (!v) return;\n              if (playSet.has(idx)) {\n                v.play().catch(() => {});\n              } else {\n                v.pause();\n              }\n            });\n          }\n        }\n\n        animId = requestAnimationFrame(frame);\n      };\n\n      animId = requestAnimationFrame(frame);\n      return () => cancelAnimationFrame(animId);\n    }, [positions, responsiveRadius, autoRotate, autoRotateSpeed, blurStrength, enableHoverScale, hoverScale, maxConcurrentVideos]);\n\n    // Pointer events for drag navigation\n    const handlePointerDown = (e: React.PointerEvent) => {\n      isDragging.current = true;\n      hasMoved.current = false;\n      dragStartPos.current = { x: e.clientX, y: e.clientY };\n      lastMousePos.current = { x: e.clientX, y: e.clientY };\n      velocityX.current = 0;\n      velocityY.current = 0;\n      // Do NOT setPointerCapture on down! That captures pointer events and swallows clicks on child cards!\n    };\n\n    const handlePointerMove = (e: React.PointerEvent) => {\n      if (!isDragging.current) return;\n      const dx = e.clientX - lastMousePos.current.x;\n      const dy = e.clientY - lastMousePos.current.y;\n      const totalDist = Math.hypot(e.clientX - dragStartPos.current.x, e.clientY - dragStartPos.current.y);\n\n      // Only engage drag rotation if moved more than 7px\n      if (totalDist > 7) {\n        hasMoved.current = true;\n        setIsGrabbing(true);\n        if (capturedPointerId.current === null) {\n          try {\n            (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);\n            capturedPointerId.current = e.pointerId;\n          } catch {}\n        }\n      }\n\n      if (hasMoved.current) {\n        velocityX.current = dx * 0.004;\n        velocityY.current = dy * 0.004;\n        rotY.current += dx * 0.004;\n        rotX.current += dy * 0.004;\n        rotX.current = Math.max(-1.35, Math.min(1.35, rotX.current));\n      }\n      lastMousePos.current = { x: e.clientX, y: e.clientY };\n    };\n\n    const handlePointerUp = (e: React.PointerEvent) => {\n      if (!isDragging.current) return;\n      isDragging.current = false;\n      setIsGrabbing(false);\n\n      if (capturedPointerId.current !== null) {\n        try {\n          (e.currentTarget as HTMLElement).releasePointerCapture(capturedPointerId.current);\n        } catch {}\n        capturedPointerId.current = null;\n      }\n\n      // Momentum carry only if user actually dragged\n      if (hasMoved.current) {\n        rotY.current += velocityX.current * 8;\n        rotX.current += velocityY.current * 8;\n        rotX.current = Math.max(-1.35, Math.min(1.35, rotX.current));\n      } else if (cardPressRef.current) {\n        // Direct tap/click fallback if pointer release was captured\n        const press = cardPressRef.current;\n        const dist = Math.hypot(e.clientX - press.x, e.clientY - press.y);\n        const elapsed = Date.now() - press.time;\n        if (dist < 15 && elapsed < 800) {\n          handleCardSelect(press.item, press.idx);\n        }\n      }\n\n      cardPressRef.current = null;\n      velocityX.current = 0;\n      velocityY.current = 0;\n    };\n\n    const handleWheel = (e: React.WheelEvent) => {\n      rotY.current += e.deltaX * 0.001;\n      rotX.current += e.deltaY * 0.001;\n      rotX.current = Math.max(-1.35, Math.min(1.35, rotX.current));\n    };\n\n    const handleCardSelect = (item: VideoSphereItem, idx: number) => {\n      if (onCardClick) onCardClick(item, idx);\n      if (enablePopup) setSelectedItem(item);\n    };\n\n    // Keyboard navigation (Escape to close modal)\n    useEffect(() => {\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (e.key === \"Escape\") setSelectedItem(null);\n      };\n      window.addEventListener(\"keydown\", handleKeyDown);\n      return () => window.removeEventListener(\"keydown\", handleKeyDown);\n    }, []);\n\n    return (\n      <div\n        ref={(node) => {\n          (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n        }}\n        className={cn(\n          \"relative flex h-[460px] sm:h-[560px] md:h-[640px] lg:h-[700px] w-full items-center justify-center overflow-hidden select-none bg-background text-foreground transition-colors duration-300\",\n          className,\n        )}\n        style={{ ...accentStyle(color), ...style }}\n        {...props}\n      >\n        {/* Ambient radial glow matching Framer */}\n        <div\n          style={{\n            position: \"absolute\",\n            width: \"60%\",\n            height: \"60%\",\n            top: \"20%\",\n            left: \"20%\",\n            borderRadius: \"50%\",\n            background: \"var(--color-accent, rgba(99, 102, 241, 0.09))\",\n            filter: \"blur(120px)\",\n            pointerEvents: \"none\",\n          }}\n        />\n\n        {/* Grain / noise texture overlay matching Framer */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            pointerEvents: \"none\",\n            opacity: 0.03,\n            backgroundImage: \"url('https://grainy-gradients.vercel.app/noise.svg')\",\n            mixBlendMode: \"overlay\",\n          }}\n        />\n\n        {/* 3D Sphere Interactive Stage */}\n        <div\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={handlePointerUp}\n          onPointerCancel={handlePointerUp}\n          onWheel={handleWheel}\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            perspective: `${responsivePerspective}px`,\n            perspectiveOrigin: \"50% 50%\",\n            transformStyle: \"preserve-3d\",\n            cursor: isGrabbing ? \"grabbing\" : \"grab\",\n            touchAction: \"none\",\n          }}\n        >\n          <div\n            style={{\n              position: \"relative\",\n              pointerEvents: \"none\",\n              transformStyle: \"preserve-3d\",\n            }}\n          >\n            {displayItems.map((item, idx) => {\n              const isVideo = item.type === \"video\" || (!item.type && !item.src.match(/\\.(jpg|jpeg|png|webp|gif|svg)$/i));\n\n              return (\n                <div\n                  key={(item.id || item.src) + idx}\n                  ref={(el) => {\n                    cardRefs.current[idx] = el;\n                  }}\n                  onMouseEnter={() => {\n                    setHoveredIndex(idx);\n                    if (unmuteOnHover && videoRefs.current[idx]) {\n                      videoRefs.current[idx]!.muted = false;\n                    }\n                  }}\n                  onMouseLeave={() => {\n                    setHoveredIndex(null);\n                    if (unmuteOnHover && videoRefs.current[idx]) {\n                      videoRefs.current[idx]!.muted = true;\n                    }\n                  }}\n                  onPointerDown={(e) => {\n                    cardPressRef.current = {\n                      item,\n                      idx,\n                      x: e.clientX,\n                      y: e.clientY,\n                      time: Date.now(),\n                    };\n                  }}\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    if (!hasMoved.current) {\n                      handleCardSelect(item, idx);\n                    }\n                  }}\n                  style={{\n                    position: \"absolute\",\n                    top: \"50%\",\n                    left: \"50%\",\n                    width: responsiveWidth,\n                    height: responsiveHeight,\n                    borderRadius: responsiveWidth < 85 ? \"10px\" : \"12px\",\n                    overflow: \"hidden\",\n                    backgroundColor: \"#111\",\n                    border: `1.5px solid ${cardBorderColor || \"var(--primary)\"}`,\n                    boxShadow: \"none\",\n                    cursor: \"pointer\",\n                    pointerEvents: \"auto\",\n                    willChange: \"transform, opacity, filter\",\n                  }}\n                >\n                  {isVideo ? (\n                    <video\n                      ref={(el) => {\n                        videoRefs.current[idx] = el;\n                      }}\n                      src={item.src}\n                      poster={item.poster}\n                      muted={muted}\n                      autoPlay\n                      loop={loop}\n                      playsInline\n                      preload=\"metadata\"\n                      style={{\n                        width: \"100%\",\n                        height: \"100%\",\n                        objectFit: \"cover\",\n                        userSelect: \"none\",\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  ) : (\n                    <img\n                      src={item.src}\n                      alt={item.title || \"\"}\n                      style={{\n                        width: \"100%\",\n                        height: \"100%\",\n                        objectFit: \"cover\",\n                        userSelect: \"none\",\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  )}\n\n                  {/* Subtle bottom gradient overlay */}\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      inset: 0,\n                      background: \"linear-gradient(to top, rgba(0,0,0,0.5), transparent)\",\n                      pointerEvents: \"none\",\n                    }}\n                  />\n                </div>\n              );\n            })}\n          </div>\n        </div>\n\n        {/* Fullscreen Video / Image Preview Lightbox Modal */}\n        {mounted && selectedItem && enablePopup && typeof document !== \"undefined\" && createPortal(\n          <div\n            onClick={() => setSelectedItem(null)}\n            className=\"fixed inset-0 z-[99999] flex items-center justify-center bg-background/80 dark:bg-black/85 backdrop-blur-md p-3 sm:p-6 md:p-8 animate-in fade-in duration-200 cursor-pointer\"\n          >\n            <div\n              onClick={(e) => e.stopPropagation()}\n              className=\"relative max-w-5xl max-h-[90vh] w-auto h-auto overflow-hidden rounded-2xl bg-card text-card-foreground shadow-2xl animate-in zoom-in-95 duration-200 cursor-default flex flex-col items-center justify-center border border-border\"\n              style={{\n                borderColor: cardBorderColor || undefined,\n              }}\n            >\n              {/* Close Button with mobile-friendly touch area */}\n              <button\n                type=\"button\"\n                onClick={() => setSelectedItem(null)}\n                className=\"absolute top-3 right-3 z-30 size-9 sm:size-10 rounded-full bg-background/80 hover:bg-background text-foreground border border-border flex items-center justify-center transition-colors cursor-pointer shadow-md backdrop-blur-sm\"\n                aria-label=\"Close preview\"\n              >\n                <X className=\"size-4 sm:size-5\" />\n              </button>\n\n              {/* Media container */}\n              <div className=\"relative max-h-[82vh] max-w-[92vw] overflow-hidden flex items-center justify-center bg-black/5 dark:bg-black rounded-2xl\">\n                {selectedItem.type === \"video\" || (!selectedItem.type && !selectedItem.src.match(/\\.(jpg|jpeg|png|webp|gif|svg)$/i)) ? (\n                  <video\n                    key={selectedItem.src}\n                    src={selectedItem.src}\n                    poster={selectedItem.poster}\n                    autoPlay\n                    controls\n                    loop\n                    playsInline\n                    className=\"max-h-[82vh] max-w-[92vw] w-auto h-auto object-contain rounded-2xl\"\n                    ref={(el) => {\n                      if (el) {\n                        el.play().catch(() => {\n                          el.muted = true;\n                          el.play().catch(() => {});\n                        });\n                      }\n                    }}\n                  />\n                ) : (\n                  <img\n                    key={selectedItem.src}\n                    src={selectedItem.src}\n                    alt={selectedItem.title || \"\"}\n                    className=\"max-h-[82vh] max-w-[92vw] w-auto h-auto object-contain rounded-2xl\"\n                  />\n                )}\n              </div>\n\n              {/* Title footer if available */}\n              {selectedItem.title && (\n                <div className=\"absolute inset-x-0 bottom-0 z-20 p-4 sm:p-5 bg-gradient-to-t from-black/90 via-black/50 to-transparent pointer-events-none rounded-b-2xl\">\n                  <h4 className=\"text-sm sm:text-base font-semibold text-white drop-shadow\">\n                    {selectedItem.title}\n                  </h4>\n                  {selectedItem.description && (\n                    <p className=\"text-xs sm:text-sm text-white/80 mt-1 drop-shadow leading-relaxed line-clamp-2\">\n                      {selectedItem.description}\n                    </p>\n                  )}\n                </div>\n              )}\n            </div>\n          </div>,\n          document.body,\n        )}\n      </div>\n    );\n  },\n);\n\nVideoSphereGallery.displayName = \"VideoSphereGallery\";\n\n// ------------------------------------------\n// Root Gallery\n// ------------------------------------------\nexport const Gallery = forwardRef<HTMLElement, GalleryProps>(\n  (\n    {\n      variant = \"orbit\",\n      images = DEFAULT_IMAGES,\n      duration = 36,\n      color,\n      className,\n      style,\n      onMouseMove,\n      onMouseEnter,\n      onMouseLeave,\n      // Bento specific\n      items = DEFAULT_BENTO_ITEMS,\n      swapInterval = 3000,\n      gap = 12,\n      autoPlay = true,\n      // Reel specific\n      reelItems = DEFAULT_CIRCULAR_REEL_ITEMS,\n      idleRotationSpeed = 5,\n      scrollSensitivity = 0.06,\n      ringThickness = 37,\n      grayscale = false,\n      // 3D Pill Carousel specific\n      pillItems = DEFAULT_PILL_CAROUSEL_ITEMS,\n      cardBorderRadius = 200,\n      activeBorderWidth = 6,\n      activeBorderColor,\n      transitionDuration = 0.6,\n      hoverScale = 1.05,\n      hoverBrightness = 1.15,\n      autoSlide = true,\n      autoPlayInterval = 3000,\n      tooltipText = \"View Details\",\n      tooltipBackgroundColor,\n      tooltipTextColor,\n      // Arc Carousel specific\n      arcItems = DEFAULT_ARC_CAROUSEL_ITEMS,\n      cardSize = 220,\n      cardHeight,\n      curve = -200,\n      perspective = 1200,\n      autoScrollSpeed = 0.22,\n      dragSpeed = 1.2,\n      inertia = 0.75,\n      borderRadius = 20,\n      // Video Sphere specific\n      sphereItems = DEFAULT_VIDEO_SPHERE_ITEMS,\n      radius = 280,\n      itemCount = 35,\n      autoRotateSpeed = 0.0015,\n      itemWidth = 110,\n      itemHeight = 150,\n      blurStrength = 12,\n      unmuteOnHover = false,\n      maxConcurrentVideos = 12,\n      enableHoverScale = true,\n      enablePopup = true,\n      popupWidth = 400,\n      popupHeight = 500,\n      cardBorderColor,\n      onCardClick,\n      ...props\n    },\n    ref,\n  ) => {\n    const count = images.length;\n\n    // ----------------------------------------\n    // VARIANT: VIDEO SPHERE GALLERY\n    // ----------------------------------------\n    if (variant === \"video-sphere\" || variant === \"video-sphere-gallery\") {\n      return (\n        <section\n          ref={ref}\n          onMouseMove={onMouseMove}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          style={{ ...accentStyle(color), ...style }}\n          className={cn(\n            \"relative flex w-full select-none items-center justify-center overflow-hidden bg-background text-foreground transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"relative z-10 w-full\">\n            <VideoSphereGallery\n              items={sphereItems}\n              radius={radius}\n              itemCount={itemCount}\n              autoRotateSpeed={autoRotateSpeed}\n              itemWidth={itemWidth}\n              itemHeight={itemHeight}\n              perspective={perspective}\n              blurStrength={blurStrength}\n              autoRotate={autoPlay}\n              muted={true}\n              loop={true}\n              unmuteOnHover={unmuteOnHover}\n              maxConcurrentVideos={maxConcurrentVideos}\n              hoverScale={hoverScale}\n              enableHoverScale={enableHoverScale}\n              enablePopup={enablePopup}\n              popupWidth={popupWidth}\n              popupHeight={popupHeight}\n              cardBorderColor={cardBorderColor}\n              color={color}\n              onCardClick={onCardClick}\n            />\n          </div>\n        </section>\n      );\n    }\n\n    // ----------------------------------------\n    // VARIANT: 3D PILL CAROUSEL\n    // ----------------------------------------\n    if (variant === \"pill-carousel\") {\n      return (\n        <section\n          ref={ref}\n          onMouseMove={onMouseMove}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          style={{ ...accentStyle(color), ...style }}\n          className={cn(\n            \"relative flex w-full select-none items-center justify-center overflow-hidden bg-background text-foreground transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"relative z-10 w-full\">\n            <ThreeDPillCarousel\n              items={pillItems}\n              color={color}\n              cardBorderRadius={cardBorderRadius}\n              activeBorderWidth={activeBorderWidth}\n              activeBorderColor={activeBorderColor}\n              transitionDuration={transitionDuration}\n              hoverScale={hoverScale}\n              hoverBrightness={hoverBrightness}\n              autoSlide={autoSlide}\n              autoPlay={autoPlay}\n              autoPlayInterval={autoPlayInterval}\n              tooltipText={tooltipText}\n              tooltipBackgroundColor={tooltipBackgroundColor}\n              tooltipTextColor={tooltipTextColor}\n            />\n          </div>\n        </section>\n      );\n    }\n\n    // ----------------------------------------\n    // VARIANT: ARC CAROUSEL GALLERY\n    // ----------------------------------------\n    if (variant === \"arc-carousel\" || variant === \"arc-carousel-gallery\") {\n      return (\n        <section\n          ref={ref}\n          onMouseMove={onMouseMove}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          style={{ ...accentStyle(color), ...style }}\n          className={cn(\n            \"relative flex w-full select-none items-center justify-center overflow-hidden bg-background text-foreground transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"relative z-10 w-full\">\n            <ArcCarouselGallery\n              items={arcItems}\n              color={color}\n              cardSize={cardSize}\n              cardHeight={cardHeight}\n              gap={gap}\n              curve={curve}\n              perspective={perspective}\n              autoScrollSpeed={autoScrollSpeed}\n              autoSlide={autoSlide}\n              autoPlay={autoPlay}\n              dragSpeed={dragSpeed}\n              inertia={inertia}\n              borderRadius={borderRadius}\n            />\n          </div>\n        </section>\n      );\n    }\n\n    // ----------------------------------------\n    // VARIANT: CIRCULAR REEL\n    // ----------------------------------------\n    if (variant === \"circular-reel\") {\n      return (\n        <section\n          ref={ref}\n          onMouseMove={onMouseMove}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          style={{ ...accentStyle(color), ...style }}\n          className={cn(\n            \"relative flex w-full select-none items-center justify-center overflow-hidden\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"relative z-10 w-full\">\n            <CircularReel\n              items={reelItems}\n              color={color}\n              idleRotationSpeed={idleRotationSpeed}\n              scrollSensitivity={scrollSensitivity}\n              ringThickness={ringThickness}\n              grayscale={grayscale}\n              autoPlay={autoPlay}\n            />\n          </div>\n        </section>\n      );\n    }\n\n    // ----------------------------------------\n    // VARIANT: MOVING BENTO\n    // ----------------------------------------\n    if (variant === \"moving-bento\") {\n      return (\n        <section\n          ref={ref}\n          onMouseMove={onMouseMove}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          style={{ ...accentStyle(color), ...style }}\n          className={cn(\n            \"relative flex w-full select-none items-center justify-center overflow-hidden bg-background p-4 sm:p-6 text-foreground transition-colors duration-300\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"relative z-10 w-full\">\n            <MovingBento\n              items={items}\n              swapInterval={swapInterval}\n              gap={gap}\n              autoPlay={autoPlay}\n              color={color}\n            />\n          </div>\n        </section>\n      );\n    }\n\n    // ----------------------------------------\n    // VARIANT: ORBIT (DEFAULT)\n    // ----------------------------------------\n    return (\n      <section\n        ref={ref}\n        onMouseMove={onMouseMove}\n        onMouseEnter={onMouseEnter}\n        onMouseLeave={onMouseLeave}\n        style={{ ...accentStyle(color), ...style }}\n        className={cn(\n          \"relative flex min-h-[600px] w-full select-none items-center overflow-hidden bg-background py-10 text-foreground transition-colors duration-300 sm:min-h-[660px] sm:py-14 lg:min-h-[720px] lg:py-20\",\n          className,\n        )}\n        {...props}\n      >\n\n        {/* The orbit: every card is rotated about the ring's centre, pushed out\n            by the radius, then counter-rotated so it stays upright. The radius\n            grows with the breakpoint. */}\n        <style>{`\n          .orbit-gallery-ring {\n            transform-style: preserve-3d;\n            perspective: 3000px;\n          }\n          .orbit-gallery-card {\n            --orbit-r: 110px;\n            backface-visibility: hidden;\n            will-change: transform;\n            animation: orbit-gallery-slide var(--orbit-duration, 36s) linear infinite;\n          }\n          @media (min-width: 480px) { .orbit-gallery-card { --orbit-r: 135px; } }\n          @media (min-width: 640px) { .orbit-gallery-card { --orbit-r: 185px; } }\n          @media (min-width: 1024px) { .orbit-gallery-card { --orbit-r: 220px; } }\n          @keyframes orbit-gallery-slide {\n            0%   { transform: rotate(0deg)   translateY(calc(-1 * var(--orbit-r))) rotateY(2.5deg) rotate(0deg); }\n            25%  { transform: rotate(90deg)  translateY(calc(-1 * var(--orbit-r))) rotateY(2.5deg) rotate(-90deg); }\n            50%  { transform: rotate(180deg) translateY(calc(-1 * var(--orbit-r))) rotateY(2.5deg) rotate(-180deg); }\n            75%  { transform: rotate(270deg) translateY(calc(-1 * var(--orbit-r))) rotateY(2.5deg) rotate(-270deg); }\n            100% { transform: rotate(360deg) translateY(calc(-1 * var(--orbit-r))) rotateY(2.5deg) rotate(-360deg); }\n          }\n          .orbit-gallery-wrapper:hover .orbit-gallery-card,\n          .orbit-gallery-ring:hover .orbit-gallery-card,\n          .orbit-gallery-card:hover {\n            animation-play-state: paused;\n          }\n          .orbit-gallery-card:hover {\n            z-index: 50;\n          }\n          .orbit-gallery-card:hover > div {\n            transform: translateZ(30px) scale(1.15) !important;\n          }\n          @media (prefers-reduced-motion: reduce) {\n            .orbit-gallery-card { animation-play-state: paused; }\n          }\n        `}</style>\n\n        <div className=\"relative z-10 mx-auto flex w-full items-center justify-center px-4 sm:px-6 md:px-10\">\n          {/* ---- The ring ---- */}\n          <div className=\"orbit-gallery-wrapper relative flex h-[480px] w-full items-center justify-center overflow-visible py-4 sm:h-[580px] lg:h-[660px]\">\n            <div\n              className=\"orbit-gallery-ring pointer-events-auto relative flex size-[340px] cursor-pointer items-center justify-center overflow-visible rounded-full sm:size-[440px] lg:size-[480px]\"\n              style={{ \"--orbit-duration\": `${duration}s` } as React.CSSProperties}\n            >\n              <div className=\"pointer-events-none absolute inset-0 rounded-full border border-transparent\" aria-hidden />\n\n              {images.map((src, i) => (\n                <div\n                  key={src + i}\n                  data-index={i}\n                  className=\"orbit-gallery-card group pointer-events-auto absolute left-1/2 top-1/2 -ml-[38px] -mt-[48px] h-[96px] w-[76px] sm:-ml-[62px] sm:-mt-[77px] sm:h-[155px] sm:w-[125px] lg:-ml-[75px] lg:-mt-[90px] lg:h-[180px] lg:w-[150px]\"\n                  style={{ animationDelay: `${-((i / count) * duration)}s` }}\n                >\n                  <div\n                    className=\"h-full w-full overflow-hidden rounded-[22px] border-2 border-card bg-card shadow-xl sm:rounded-[28px] transition-transform duration-300 ease-out group-hover:scale-115 group-hover:shadow-2xl cursor-pointer\"\n                    style={{ transformOrigin: \"center center\", willChange: \"transform\" }}\n                  >\n                    <img\n                      src={src}\n                      alt=\"\"\n                      draggable={false}\n                      className=\"size-full object-cover pointer-events-none select-none transition-transform duration-300 ease-out group-hover:scale-105\"\n                    />\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      </section>\n    );\n  },\n);\n\nGallery.displayName = \"Gallery\";\n\nexport const OrbitGallery = Gallery;\n\nexport default Gallery;\n\n",
      "type": "registry:component",
      "target": "components/custom/blocks/GalleryBlock.tsx"
    }
  ],
  "docs": "Named export: Gallery (also exports OrbitGallery for backward compatibility). Default export: Gallery. Variants: orbit | bento | reel | pill | arc. Pass items ({ image, alt, title?, subtitle?, href? }), speed/duration, orientation, aspect, autoSlide, and radius/gap options.",
  "categories": [
    "media",
    "scroll",
    "layout"
  ],
  "type": "registry:component"
}
