{"slug":"shader-background","name":"Shader Background","description":"Canvas shader backgrounds (mesh gradient, grain, warp, waves, voronoi, dot orbit and more) with a single typed variant prop. Reduced-motion freezes animated variants.","category":"motion","source_url":"https://beui.dev/r/shader-background/raw","detail_url":"https://beui.dev/r/shader-background","raw_url":"https://beui.dev/r/shader-background/raw","page_url":"https://beui.dev/components/motion/shader-background","markdown_url":"https://beui.dev/components/motion/shader-background.md","published_at":"2026-07-02","updated_at":"2026-09-11","dependencies":["@paper-design/shaders-react","clsx","lucide-react","motion","react","tailwind-merge"],"internal":["@/components/motion/shader-background","@/components/motion/tabs","@/lib/ease","@/lib/utils"],"files":[{"path":"components/motion/shader-background.tsx","type":"component","content":"\"use client\";\n// beui.dev/components/motion/shader-background\n\nimport {\n  ColorPanels,\n  type ColorPanelsProps,\n  Dithering,\n  type DitheringProps,\n  DotGrid,\n  type DotGridProps,\n  DotOrbit,\n  type DotOrbitProps,\n  GodRays,\n  type GodRaysProps,\n  GrainGradient,\n  type GrainGradientProps,\n  Metaballs,\n  type MetaballsProps,\n  MeshGradient,\n  type MeshGradientProps,\n  NeuroNoise,\n  type NeuroNoiseProps,\n  PerlinNoise,\n  type PerlinNoiseProps,\n  PulsingBorder,\n  type PulsingBorderProps,\n  SimplexNoise,\n  type SimplexNoiseProps,\n  SmokeRing,\n  type SmokeRingProps,\n  Spiral,\n  type SpiralProps,\n  StaticMeshGradient,\n  type StaticMeshGradientProps,\n  StaticRadialGradient,\n  type StaticRadialGradientProps,\n  Swirl,\n  type SwirlProps,\n  Voronoi,\n  type VoronoiProps,\n  Warp,\n  type WarpProps,\n  Water,\n  type WaterProps,\n  Waves,\n  type WavesProps,\n} from \"@paper-design/shaders-react\";\nimport { useReducedMotion } from \"motion/react\";\nimport type { ComponentType } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype ShaderVariantProps = {\n  \"mesh-gradient\": MeshGradientProps;\n  \"grain-gradient\": GrainGradientProps;\n  \"dot-grid\": DotGridProps;\n  \"dot-orbit\": DotOrbitProps;\n  warp: WarpProps;\n  waves: WavesProps;\n  water: WaterProps;\n  voronoi: VoronoiProps;\n  swirl: SwirlProps;\n  \"smoke-ring\": SmokeRingProps;\n  \"static-radial-gradient\": StaticRadialGradientProps;\n  \"neuro-noise\": NeuroNoiseProps;\n  metaballs: MetaballsProps;\n  \"god-rays\": GodRaysProps;\n  spiral: SpiralProps;\n  dithering: DitheringProps;\n  \"pulsing-border\": PulsingBorderProps;\n  \"color-panels\": ColorPanelsProps;\n  \"static-mesh-gradient\": StaticMeshGradientProps;\n  \"simplex-noise\": SimplexNoiseProps;\n  \"perlin-noise\": PerlinNoiseProps;\n};\n\nexport type ShaderBackgroundVariant = keyof ShaderVariantProps;\n\nexport type ShaderBackgroundProps = {\n  [K in ShaderBackgroundVariant]: { variant: K } & ShaderVariantProps[K];\n}[ShaderBackgroundVariant];\n\nconst VARIANT_COMPONENTS: {\n  [K in ShaderBackgroundVariant]: ComponentType<ShaderVariantProps[K]>;\n} = {\n  \"mesh-gradient\": MeshGradient,\n  \"grain-gradient\": GrainGradient,\n  \"dot-grid\": DotGrid,\n  \"dot-orbit\": DotOrbit,\n  warp: Warp,\n  waves: Waves,\n  water: Water,\n  voronoi: Voronoi,\n  swirl: Swirl,\n  \"smoke-ring\": SmokeRing,\n  \"static-radial-gradient\": StaticRadialGradient,\n  \"neuro-noise\": NeuroNoise,\n  metaballs: Metaballs,\n  \"god-rays\": GodRays,\n  spiral: Spiral,\n  dithering: Dithering,\n  \"pulsing-border\": PulsingBorder,\n  \"color-panels\": ColorPanels,\n  \"static-mesh-gradient\": StaticMeshGradient,\n  \"simplex-noise\": SimplexNoise,\n  \"perlin-noise\": PerlinNoise,\n};\n\nexport const SHADER_BACKGROUND_VARIANTS = Object.keys(\n  VARIANT_COMPONENTS,\n) as ShaderBackgroundVariant[];\n\n/**\n * Not every variant animates (e.g. dot-grid is a static pattern), so `speed`\n * is only frozen for reduced motion when the variant actually exposes it.\n */\nexport function ShaderBackground({\n  variant,\n  className,\n  ...rest\n}: ShaderBackgroundProps) {\n  const reducedMotion = useReducedMotion();\n  const Shader = VARIANT_COMPONENTS[variant] as ComponentType<\n    Record<string, unknown>\n  >;\n  const props = rest as Record<string, unknown>;\n  const speedProps = reducedMotion && \"speed\" in props ? { speed: 0 } : {};\n\n  return (\n    <Shader\n      {...props}\n      {...speedProps}\n      className={cn(\"h-full w-full\", className)}\n    />\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/previews/motion/shader-background.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport type { ComponentType } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport {\n  ShaderBackground,\n  type ShaderBackgroundProps,\n  type ShaderBackgroundVariant,\n} from \"@/components/motion/shader-background\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/motion/tabs\";\n\nconst AUTOPLAY_MS = 2400;\n\nconst VARIANTS: {\n  id: string;\n  value: ShaderBackgroundVariant;\n  label: string;\n  props: Record<string, unknown>;\n}[] = [\n  {\n    id: \"mesh-gradient\",\n    value: \"mesh-gradient\",\n    label: \"Mesh\",\n    props: {\n      colors: [\"#e0eaff\", \"#241d9a\", \"#f75092\", \"#9f50d3\"],\n      distortion: 0.8,\n      swirl: 0.3,\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"grain-gradient\",\n    value: \"grain-gradient\",\n    label: \"Grain\",\n    props: {\n      colors: [\"#7300ff\", \"#eba8ff\", \"#00bfff\", \"#2a00ff\"],\n      colorBack: \"#000000\",\n      softness: 0.6,\n      speed: 0.5,\n    },\n  },\n  {\n    id: \"grain-sunset\",\n    value: \"grain-gradient\",\n    label: \"Grain — Sunset\",\n    props: {\n      colors: [\"#ff7a00\", \"#ff2e93\", \"#ffce54\", \"#8a2be2\"],\n      colorBack: \"#1a0500\",\n      softness: 0.7,\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"grain-pastel\",\n    value: \"grain-gradient\",\n    label: \"Grain — Pastel\",\n    props: {\n      colors: [\"#ffd6e8\", \"#c9e4ff\", \"#fff3c4\", \"#d9c9ff\"],\n      colorBack: \"#ffffff\",\n      softness: 0.85,\n      speed: 0.3,\n    },\n  },\n  {\n    id: \"mesh-aurora\",\n    value: \"mesh-gradient\",\n    label: \"Mesh — Aurora\",\n    props: {\n      colors: [\"#00ffb2\", \"#0072ff\", \"#a200ff\", \"#001a2c\"],\n      distortion: 0.6,\n      swirl: 0.5,\n      speed: 0.3,\n    },\n  },\n  {\n    id: \"mesh-citrus\",\n    value: \"mesh-gradient\",\n    label: \"Mesh — Citrus\",\n    props: {\n      colors: [\"#fff200\", \"#ff8a00\", \"#ff3d00\", \"#ffe08a\"],\n      distortion: 0.7,\n      swirl: 0.4,\n      speed: 0.5,\n    },\n  },\n  {\n    id: \"warp\",\n    value: \"warp\",\n    label: \"Warp\",\n    props: {\n      colors: [\"#121212\", \"#9470ff\", \"#121212\", \"#8838ff\"],\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"waves\",\n    value: \"waves\",\n    label: \"Waves\",\n    props: { colorFront: \"#ffbb00\", colorBack: \"#000000\" },\n  },\n  {\n    id: \"voronoi\",\n    value: \"voronoi\",\n    label: \"Voronoi\",\n    props: { colors: [\"#ff8247\", \"#ffe53d\"], speed: 0.3 },\n  },\n  {\n    id: \"swirl\",\n    value: \"swirl\",\n    label: \"Swirl\",\n    props: {\n      colorBack: \"#180018\",\n      colors: [\"#ffd1d1\", \"#ff8a8a\", \"#660000\"],\n      speed: 0.2,\n    },\n  },\n  {\n    id: \"dot-orbit\",\n    value: \"dot-orbit\",\n    label: \"Orbit\",\n    props: {\n      colors: [\"#ffc96b\", \"#ff6200\", \"#ff2f00\"],\n      colorBack: \"#000000\",\n      speed: 0.6,\n    },\n  },\n  {\n    id: \"dot-grid\",\n    value: \"dot-grid\",\n    label: \"Grid\",\n    props: {\n      colorBack: \"#000000\",\n      colorFill: \"#ffffff\",\n      colorStroke: \"#ffaa00\",\n    },\n  },\n  {\n    id: \"smoke-ring\",\n    value: \"smoke-ring\",\n    label: \"Smoke\",\n    props: { colorBack: \"#000000\", colors: [\"#ffffff\"], speed: 0.3 },\n  },\n  {\n    id: \"static-radial-gradient\",\n    value: \"static-radial-gradient\",\n    label: \"Radial\",\n    props: { colorBack: \"#000000\", colors: [\"#00bbff\", \"#00ffe1\", \"#ffffff\"] },\n  },\n  {\n    id: \"neuro-noise\",\n    value: \"neuro-noise\",\n    label: \"Neuro\",\n    props: {\n      colorFront: \"#ffffff\",\n      colorMid: \"#47a6ff\",\n      colorBack: \"#000000\",\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"water\",\n    value: \"water\",\n    label: \"Water\",\n    props: { colorBack: \"#909090\", colorHighlight: \"#ffffff\", speed: 0.4 },\n  },\n  {\n    id: \"metaballs\",\n    value: \"metaballs\",\n    label: \"Metaballs\",\n    props: {\n      colors: [\"#ff5cf4\", \"#4d9eff\", \"#000000\"],\n      colorBack: \"#000000\",\n      speed: 0.5,\n    },\n  },\n  {\n    id: \"god-rays\",\n    value: \"god-rays\",\n    label: \"Rays\",\n    props: {\n      colors: [\"#ffcc66\", \"#ff6a00\"],\n      colorBack: \"#000000\",\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"spiral\",\n    value: \"spiral\",\n    label: \"Spiral\",\n    props: { colors: [\"#7a5cff\", \"#ff5ca6\", \"#000000\"], speed: 0.4 },\n  },\n  {\n    id: \"dithering\",\n    value: \"dithering\",\n    label: \"Dither\",\n    props: {\n      colorBack: \"#000000\",\n      colorFront: \"#00ff9d\",\n      speed: 0.4,\n    },\n  },\n  {\n    id: \"pulsing-border\",\n    value: \"pulsing-border\",\n    label: \"Pulse\",\n    props: {\n      colors: [\"#00e5ff\", \"#7000ff\", \"#ff00c8\"],\n      colorBack: \"#000000\",\n      speed: 0.5,\n    },\n  },\n  {\n    id: \"color-panels\",\n    value: \"color-panels\",\n    label: \"Panels\",\n    props: { colors: [\"#ff3d68\", \"#ffb800\", \"#3d7aff\", \"#00ffb2\"], speed: 0.3 },\n  },\n  {\n    id: \"static-mesh-gradient\",\n    value: \"static-mesh-gradient\",\n    label: \"Static Mesh\",\n    props: { colors: [\"#ff8a3d\", \"#ff3d9a\", \"#3d5aff\", \"#0a0a0a\"] },\n  },\n  {\n    id: \"static-mesh-dusk\",\n    value: \"static-mesh-gradient\",\n    label: \"Static Mesh — Dusk\",\n    props: { colors: [\"#2b1055\", \"#7597de\", \"#f6a1c8\", \"#0d0221\"] },\n  },\n  {\n    id: \"radial-ember\",\n    value: \"static-radial-gradient\",\n    label: \"Radial — Ember\",\n    props: { colorBack: \"#0a0000\", colors: [\"#ff5100\", \"#ffce00\", \"#4a0000\"] },\n  },\n  {\n    id: \"simplex-noise\",\n    value: \"simplex-noise\",\n    label: \"Simplex\",\n    props: { colors: [\"#ff6ec7\", \"#6ec7ff\", \"#000000\"], speed: 0.4 },\n  },\n  {\n    id: \"perlin-noise\",\n    value: \"perlin-noise\",\n    label: \"Perlin\",\n    props: { colorFront: \"#00ffd5\", colorBack: \"#000000\", speed: 0.3 },\n  },\n];\n\n// Every variant carries a different prop shape; the switcher only ever spreads\n// each entry's own preset, so the loose cast here is safe.\nconst Background = ShaderBackground as ComponentType<\n  ShaderBackgroundProps & Record<string, unknown>\n>;\n\nexport function ShaderBackgroundPreview() {\n  const [active, setActive] = useState<string>(VARIANTS[0].id);\n  const current = VARIANTS.find((v) => v.id === active) ?? VARIANTS[0];\n  const reduce = useReducedMotion();\n  const [paused, setPaused] = useState(false);\n\n  // Cycles through variants on its own so the preview reads as alive at a\n  // glance; pauses on hover/focus so picking a variant manually sticks.\n  useEffect(() => {\n    if (reduce || paused) return;\n    const id = setInterval(() => {\n      setActive((currentActive) => {\n        const i = VARIANTS.findIndex((v) => v.id === currentActive);\n        return VARIANTS[(i + 1) % VARIANTS.length].id;\n      });\n    }, AUTOPLAY_MS);\n    return () => clearInterval(id);\n  }, [reduce, paused]);\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: pause-on-hover for the autoplay timer, not a real control\n    <div\n      className=\"flex w-full max-w-2xl flex-col gap-5 p-6\"\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocus={() => setPaused(true)}\n      onBlur={() => setPaused(false)}\n    >\n      <div className=\"relative h-80 w-full overflow-hidden rounded-2xl border border-border\">\n        <Background\n          key={current.id}\n          variant={current.value}\n          className=\"absolute inset-0\"\n          {...current.props}\n        />\n      </div>\n      <Tabs value={active} onValueChange={setActive}>\n        <TabsList className=\"flex-wrap justify-center gap-2 rounded-2xl\">\n          {VARIANTS.map((v) => (\n            <TabsTrigger\n              key={v.id}\n              value={v.id}\n              className=\"px-4 py-2 text-sm\"\n            >\n              {v.label}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n      </Tabs>\n    </div>\n  );\n}\n"},{"path":"components/motion/tabs.tsx","type":"util","content":"\"use client\";\n\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cancelFrame, frame, motion, MotionConfig, useReducedMotion, type Transition } from \"motion/react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype Variant = \"pill\" | \"underline\" | \"segment\";\n\ntype Ctx = {\n  value: string;\n  setValue: (v: string) => void;\n  layoutId: string;\n  variant: Variant;\n};\n\nconst TabsCtx = createContext<Ctx | null>(null);\n\nfunction useTabs() {\n  const ctx = useContext(TabsCtx);\n  if (!ctx) throw new Error(\"Tabs.* must be used inside <Tabs>\");\n  return ctx;\n}\n\n// Settle without overshoot: a scrollable tab list would turn even a small\n// overshoot into a transient scrollbar and layout shift.\n// Scale stiffness and damping together for a quicker glide with the same feel.\nconst transition: Transition = {\n  type: \"spring\",\n  stiffness: 245,\n  damping: 36,\n  mass: 1.2,\n};\n\nexport function Tabs({\n  defaultValue,\n  value,\n  onValueChange,\n  variant = \"pill\",\n  children,\n  className,\n}: {\n  defaultValue?: string;\n  value?: string;\n  onValueChange?: (v: string) => void;\n  variant?: Variant;\n  children: ReactNode;\n  className?: string;\n}) {\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const layoutId = useId();\n  const reduce = useReducedMotion();\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const setValue = useCallback(\n    (v: string) => {\n      if (!controlled) setInternal(v);\n      onValueChange?.(v);\n    },\n    [controlled, onValueChange],\n  );\n  const contextValue = useMemo(\n    () => ({ value: current, setValue, layoutId, variant }),\n    [current, layoutId, setValue, variant],\n  );\n  return (\n    <MotionConfig transition={reduce ? { duration: 0 } : transition}>\n      <TabsCtx.Provider value={contextValue}>\n        {/* layoutRoot: the indicator's layoutId measures in page coordinates, so\n            inside fixed/scrolled containers it would replay scroll offsets as\n            movement. The pill only ever travels within the list, so scoping\n            projection to the Tabs wrapper is always correct. */}\n        <motion.div layoutRoot className={className}>\n          {children}\n        </motion.div>\n      </TabsCtx.Provider>\n    </MotionConfig>\n  );\n}\n\nconst listClasses: Record<Variant, string> = {\n  pill: \"inline-flex items-center gap-1 rounded-full bg-card p-1\",\n  underline: \"inline-flex items-center gap-1 border-b border-border\",\n  segment: \"inline-flex items-center gap-0 rounded-lg bg-card p-0.5\",\n};\n\nexport function TabsList({\n  children,\n  className,\n  wrapperClassName,\n}: {\n  children: ReactNode;\n  className?: string;\n  wrapperClassName?: string;\n}) {\n  const { variant, value } = useTabs();\n  const reduce = useReducedMotion();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n  const viewportId = useId();\n  const [edges, setEdges] = useState({ overflow: false, left: false, right: false });\n\n  const measure = useCallback(() => {\n    const root = rootRef.current;\n    const viewport = viewportRef.current;\n    if (!root || !viewport) return;\n    // Overlay controls do not reduce the viewport or change its scroll range.\n    const overflow = viewport.scrollWidth > root.clientWidth + 1;\n    const max = Math.max(0, viewport.scrollWidth - viewport.clientWidth);\n    const rtl = getComputedStyle(viewport).direction === \"rtl\";\n    // Modern browsers expose negative scrollLeft in RTL. Clamp rubber-banding.\n    const fromLeft = Math.max(0, Math.min(max, rtl ? max + viewport.scrollLeft : viewport.scrollLeft));\n    const next = { overflow, left: fromLeft > 1, right: fromLeft < max - 1 };\n    setEdges((previous) => previous.overflow === next.overflow && previous.left === next.left && previous.right === next.right ? previous : next);\n  }, []);\n\n  const reveal = useCallback((tab: HTMLElement | null) => {\n    const viewport = viewportRef.current;\n    if (!viewport || !tab) return;\n    const frame = viewport.getBoundingClientRect();\n    const item = tab.getBoundingClientRect();\n    const max = Math.max(0, viewport.scrollWidth - viewport.clientWidth);\n    const rtl = getComputedStyle(viewport).direction === \"rtl\";\n    const fromLeft = Math.max(0, Math.min(max, rtl ? max + viewport.scrollLeft : viewport.scrollLeft));\n    // Keep the selected/focused label clear of the arrows over the faded edges.\n    const left = frame.left + (fromLeft > 1 ? 36 : 0);\n    const right = frame.right - (fromLeft < max - 1 ? 36 : 0);\n    const delta = item.left < left ? item.left - left : item.right > right ? item.right - right : 0;\n    // Scroll only this viewport; scrollIntoView can also move the whole page.\n    if (delta) viewport.scrollBy({ left: delta, behavior: reduce ? \"instant\" : \"smooth\" });\n  }, [reduce]);\n\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    const viewport = viewportRef.current;\n    const list = listRef.current;\n    if (!root || !viewport || !list) return;\n    const update = () => {\n      measure();\n      reveal(list.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]'));\n    };\n    const observer = new ResizeObserver(update);\n    observer.observe(root);\n    observer.observe(viewport);\n    observer.observe(list);\n    viewport.addEventListener(\"scroll\", measure, { passive: true });\n    update();\n    return () => {\n      observer.disconnect();\n      viewport.removeEventListener(\"scroll\", measure);\n    };\n  }, [measure, reveal]);\n\n  useLayoutEffect(() => {\n    // Children may change without a resize; controlled selection must also reveal.\n    void children;\n    void value;\n    void edges.overflow;\n    measure();\n    reveal(listRef.current?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]') ?? null);\n  }, [children, value, edges.overflow, measure, reveal]);\n\n  useLayoutEffect(() => {\n    if (variant === \"underline\") return;\n    const list = listRef.current;\n    if (!list) return;\n    void children;\n    const labels = Array.from(list.querySelectorAll<HTMLElement>(\"[data-tabs-label]\"));\n    const indicator = list.querySelector<HTMLElement>(\"[data-tabs-indicator]\");\n    const target = list.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]');\n    if (!indicator || !target || target.dataset.tabsValue !== value) {\n      for (const label of labels) label.style.clipPath = \"inset(0 100% 0 0)\";\n      return;\n    }\n    let frames = 0;\n    let stillFrames = 0;\n    let previous: { left: number; right: number } | undefined;\n    const syncClips = () => {\n      const pill = (reduce ? target : indicator).getBoundingClientRect();\n      // Read ALL geometry before writing ANY masks. A loop per tab interleaved\n      // reads and writes, forcing the browser to flush styles repeatedly.\n      const clips = labels.map((label) => {\n        const bounds = label.getBoundingClientRect();\n        const left = Math.max(0, Math.min(bounds.width, pill.left - bounds.left));\n        const right = Math.max(0, Math.min(bounds.width, bounds.right - pill.right));\n        return left + right >= bounds.width ? \"inset(0 100% 0 0)\" : `inset(0 ${right}px 0 ${left}px)`;\n      });\n      labels.forEach((label, index) => {\n        if (label.style.clipPath !== clips[index]) label.style.clipPath = clips[index];\n      });\n      frames += 1;\n      stillFrames = previous && Math.abs(pill.left - previous.left) < 0.01 && Math.abs(pill.right - previous.right) < 0.01 ? stillFrames + 1 : 0;\n      previous = { left: pill.left, right: pill.right };\n      if (reduce || (frames > 2 && stillFrames >= 2)) cancelFrame(syncClips);\n    };\n    // One shared pass after Motion paints the projected pill keeps every label\n    // in sync, including labels crossed during a long or interrupted glide.\n    frame.postRender(syncClips, true);\n    return () => cancelFrame(syncClips);\n  }, [value, children, variant, reduce]);\n\n  const scroll = (direction: number) => {\n    const viewport = viewportRef.current;\n    if (viewport) viewport.scrollBy({ left: direction * viewport.clientWidth * 0.8, behavior: reduce ? \"instant\" : \"smooth\" });\n  };\n  const controlClass = \"absolute inset-y-0 z-20 inline-flex w-9 items-center justify-center text-foreground transition-opacity hover:opacity-70 focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-0\";\n  const surfaceClass = variant === \"pill\" ? \"rounded-full bg-card\" : variant === \"segment\" ? \"rounded-lg bg-card\" : \"\";\n\n  return (\n    <div ref={rootRef} className={cn(\"relative isolate flex w-full max-w-full min-w-0 items-center\", edges.overflow && surfaceClass, wrapperClassName)}>\n      {edges.overflow && (\n        <button type=\"button\" aria-label=\"Scroll tabs left\" aria-controls={viewportId} disabled={!edges.left} onClick={() => scroll(-1)} className={cn(controlClass, \"left-0 rounded-l-full\")}>\n          <ChevronLeft size={20} aria-hidden=\"true\" />\n        </button>\n      )}\n      <motion.div\n        ref={viewportRef}\n        id={viewportId}\n        layoutScroll\n        className={cn(\"w-full min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\", edges.overflow && \"[border-radius:inherit]\")}\n        style={edges.overflow ? {\n          maskImage: `linear-gradient(to right, ${edges.left ? \"transparent, black 40px\" : \"black, black 0px\"}, ${edges.right ? \"black calc(100% - 40px), transparent\" : \"black 100%\"})`,\n        } : undefined}\n        onFocusCapture={(event) => {\n          if (event.target instanceof HTMLElement && event.target.getAttribute(\"role\") === \"tab\") reveal(event.target);\n        }}\n      >\n        <div ref={listRef} role=\"tablist\" className={cn(listClasses[variant], \"w-max\", className)}>\n          {children}\n        </div>\n      </motion.div>\n      {edges.overflow && edges.left && (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 left-0 z-10 w-10 rounded-l-[inherit] backdrop-blur-[2px] [mask-image:linear-gradient(to_right,black,transparent)]\" />\n      )}\n      {edges.overflow && edges.right && (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 right-0 z-10 w-10 rounded-r-[inherit] backdrop-blur-[2px] [mask-image:linear-gradient(to_left,black,transparent)]\" />\n      )}\n      {edges.overflow && (\n        <button type=\"button\" aria-label=\"Scroll tabs right\" aria-controls={viewportId} disabled={!edges.right} onClick={() => scroll(1)} className={cn(controlClass, \"right-0 rounded-r-full\")}>\n          <ChevronRight size={20} aria-hidden=\"true\" />\n        </button>\n      )}\n    </div>\n  );\n}\n\nexport function TabsTrigger({\n  value,\n  children,\n  className,\n  indicatorClassName,\n}: {\n  value: string;\n  children: ReactNode;\n  className?: string;\n  indicatorClassName?: string;\n}) {\n  const { value: current, setValue, layoutId, variant } = useTabs();\n  const active = current === value;\n  // React owns the initial mask only; TabsList synchronizes subsequent masks.\n  const [initialClip] = useState(() => active ? \"inset(0)\" : \"inset(0 100% 0 0)\");\n\n  if (variant === \"underline\") {\n    return (\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative isolate px-3 pb-2.5 pt-1 -mb-px text-sm font-medium transition-colors min-h-[44px] inline-flex items-center whitespace-nowrap shrink-0\",\n          active ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          className,\n        )}\n      >\n        {children}\n        {active ? (\n        <motion.span\n          layoutId={layoutId}\n          layout\n          className={cn(\n            \"absolute bottom-0 left-0 right-0 h-px bg-primary\",\n            indicatorClassName,\n          )}\n        />\n        ) : null}\n      </button>\n    );\n  }\n\n  const radius = variant === \"pill\" ? \"rounded-full\" : \"rounded-md\";\n\n  return (\n    <div className=\"relative shrink-0\">\n      {active ? (\n        <motion.span\n          data-tabs-indicator=\"\"\n          layoutId={layoutId}\n          layout\n          style={{ borderRadius: variant === \"pill\" ? 9999 : 8 }}\n          className={cn(\n            \"absolute inset-0 bg-primary\",\n            radius,\n            indicatorClassName,\n          )}\n        />\n      ) : null}\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        data-tabs-value={value}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative z-10 inline-flex items-center justify-center whitespace-nowrap bg-transparent px-3.5 py-1.5 text-sm font-medium outline-none\",\n          \"text-muted-foreground hover:text-foreground\",\n          radius,\n          className,\n        )}\n      >\n        {children}\n        <span\n          data-tabs-label=\"\"\n          aria-hidden=\"true\"\n          inert\n          className=\"pointer-events-none absolute inset-0 inline-flex items-center justify-center text-primary-foreground [gap:inherit] [padding:inherit]\"\n          style={{ clipPath: initialClip }}\n        >\n          {children}\n        </span>\n      </button>\n    </div>\n  );\n}\n\nexport function TabsContent({ value, children, className }: { value: string; children: ReactNode; className?: string }) {\n  const { value: current } = useTabs();\n  const reduce = useReducedMotion();\n  const active = current === value;\n  // Inactive panels stay mounted but hidden, so their content (e.g. source\n  // code) is present in the server-rendered HTML for crawlers and assistive\n  // tech, instead of being dropped from the DOM.\n  if (!active) {\n    return (\n      <div hidden className={className}>\n        {children}\n      </div>\n    );\n  }\n  return (\n    <motion.div\n      key={value}\n      initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.18, ease: EASE_OUT }}\n      className={cn(\"mt-4\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"util","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"}]}