diff --git a/bun.lock b/bun.lock index 0ba94cb..874336f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "ui-components", "dependencies": { + "@paper-design/shaders-react": "^0.0.76", "@shikijs/transformers": "^4.2.0", "@tanstack/react-virtual": "^3.14.4", "@vercel/analytics": "^1.4.1", @@ -160,6 +161,10 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.18", "", { "os": "win32", "cpu": "x64" }, "sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg=="], + "@paper-design/shaders": ["@paper-design/shaders@0.0.76", "", {}, "sha512-AcNDY4J66YQHUfQYFInkCP7M9VOje0od7wLpOR7LtCmc532opJy6ll+h1W9zBovz8tt9U7OADUmJ/qKEXyOX/A=="], + + "@paper-design/shaders-react": ["@paper-design/shaders-react@0.0.76", "", { "dependencies": { "@paper-design/shaders": "0.0.76" }, "peerDependencies": { "@types/react": "^18 || ^19", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-uPJWrYRf6cJdO2H+fuXlahaqz0QjYglNAyUTaRfIInpzCa/d6guxBIK003soAZQFuQ035yg9FhtfFzKNWm+a5A=="], + "@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], diff --git a/components/motion/shader-background.tsx b/components/motion/shader-background.tsx new file mode 100644 index 0000000..32e5f76 --- /dev/null +++ b/components/motion/shader-background.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { + ColorPanels, + type ColorPanelsProps, + Dithering, + type DitheringProps, + DotGrid, + type DotGridProps, + DotOrbit, + type DotOrbitProps, + GodRays, + type GodRaysProps, + GrainGradient, + type GrainGradientProps, + Metaballs, + type MetaballsProps, + MeshGradient, + type MeshGradientProps, + NeuroNoise, + type NeuroNoiseProps, + PerlinNoise, + type PerlinNoiseProps, + PulsingBorder, + type PulsingBorderProps, + SimplexNoise, + type SimplexNoiseProps, + SmokeRing, + type SmokeRingProps, + Spiral, + type SpiralProps, + StaticMeshGradient, + type StaticMeshGradientProps, + StaticRadialGradient, + type StaticRadialGradientProps, + Swirl, + type SwirlProps, + Voronoi, + type VoronoiProps, + Warp, + type WarpProps, + Water, + type WaterProps, + Waves, + type WavesProps, +} from "@paper-design/shaders-react"; +import { useReducedMotion } from "motion/react"; +import type { ComponentType } from "react"; +import { cn } from "@/lib/utils"; + +type ShaderVariantProps = { + "mesh-gradient": MeshGradientProps; + "grain-gradient": GrainGradientProps; + "dot-grid": DotGridProps; + "dot-orbit": DotOrbitProps; + warp: WarpProps; + waves: WavesProps; + water: WaterProps; + voronoi: VoronoiProps; + swirl: SwirlProps; + "smoke-ring": SmokeRingProps; + "static-radial-gradient": StaticRadialGradientProps; + "neuro-noise": NeuroNoiseProps; + metaballs: MetaballsProps; + "god-rays": GodRaysProps; + spiral: SpiralProps; + dithering: DitheringProps; + "pulsing-border": PulsingBorderProps; + "color-panels": ColorPanelsProps; + "static-mesh-gradient": StaticMeshGradientProps; + "simplex-noise": SimplexNoiseProps; + "perlin-noise": PerlinNoiseProps; +}; + +export type ShaderBackgroundVariant = keyof ShaderVariantProps; + +export type ShaderBackgroundProps = { + [K in ShaderBackgroundVariant]: { variant: K } & ShaderVariantProps[K]; +}[ShaderBackgroundVariant]; + +const VARIANT_COMPONENTS: { + [K in ShaderBackgroundVariant]: ComponentType; +} = { + "mesh-gradient": MeshGradient, + "grain-gradient": GrainGradient, + "dot-grid": DotGrid, + "dot-orbit": DotOrbit, + warp: Warp, + waves: Waves, + water: Water, + voronoi: Voronoi, + swirl: Swirl, + "smoke-ring": SmokeRing, + "static-radial-gradient": StaticRadialGradient, + "neuro-noise": NeuroNoise, + metaballs: Metaballs, + "god-rays": GodRays, + spiral: Spiral, + dithering: Dithering, + "pulsing-border": PulsingBorder, + "color-panels": ColorPanels, + "static-mesh-gradient": StaticMeshGradient, + "simplex-noise": SimplexNoise, + "perlin-noise": PerlinNoise, +}; + +export const SHADER_BACKGROUND_VARIANTS = Object.keys( + VARIANT_COMPONENTS, +) as ShaderBackgroundVariant[]; + +/** + * Not every variant animates (e.g. dot-grid is a static pattern), so `speed` + * is only frozen for reduced motion when the variant actually exposes it. + */ +export function ShaderBackground({ + variant, + className, + ...rest +}: ShaderBackgroundProps) { + const reducedMotion = useReducedMotion(); + const Shader = VARIANT_COMPONENTS[variant] as ComponentType< + Record + >; + const props = rest as Record; + const speedProps = reducedMotion && "speed" in props ? { speed: 0 } : {}; + + return ( + + ); +} diff --git a/components/previews/index.tsx b/components/previews/index.tsx index 9e92be8..0ef426e 100644 --- a/components/previews/index.tsx +++ b/components/previews/index.tsx @@ -190,6 +190,11 @@ export const previews: Record = { "motion/range-slider": dynamic(() => import("./motion/range-slider.preview").then((m) => m.RangeSliderPreview), ), + "motion/shader-background": dynamic(() => + import("./motion/shader-background.preview").then( + (m) => m.ShaderBackgroundPreview, + ), + ), }; export function getPreview(category: string, slug: string) { diff --git a/components/previews/motion/shader-background.preview.tsx b/components/previews/motion/shader-background.preview.tsx new file mode 100644 index 0000000..e30d6a6 --- /dev/null +++ b/components/previews/motion/shader-background.preview.tsx @@ -0,0 +1,283 @@ +"use client"; + +import type { ComponentType } from "react"; +import { useState } from "react"; +import { + ShaderBackground, + type ShaderBackgroundProps, + type ShaderBackgroundVariant, +} from "@/components/motion/shader-background"; +import { Tabs, TabsList, TabsTrigger } from "@/components/motion/tabs"; + +const VARIANTS: { + id: string; + value: ShaderBackgroundVariant; + label: string; + props: Record; +}[] = [ + { + id: "mesh-gradient", + value: "mesh-gradient", + label: "Mesh", + props: { + colors: ["#e0eaff", "#241d9a", "#f75092", "#9f50d3"], + distortion: 0.8, + swirl: 0.3, + speed: 0.4, + }, + }, + { + id: "grain-gradient", + value: "grain-gradient", + label: "Grain", + props: { + colors: ["#7300ff", "#eba8ff", "#00bfff", "#2a00ff"], + colorBack: "#000000", + softness: 0.6, + speed: 0.5, + }, + }, + { + id: "grain-sunset", + value: "grain-gradient", + label: "Grain — Sunset", + props: { + colors: ["#ff7a00", "#ff2e93", "#ffce54", "#8a2be2"], + colorBack: "#1a0500", + softness: 0.7, + speed: 0.4, + }, + }, + { + id: "grain-pastel", + value: "grain-gradient", + label: "Grain — Pastel", + props: { + colors: ["#ffd6e8", "#c9e4ff", "#fff3c4", "#d9c9ff"], + colorBack: "#ffffff", + softness: 0.85, + speed: 0.3, + }, + }, + { + id: "mesh-aurora", + value: "mesh-gradient", + label: "Mesh — Aurora", + props: { + colors: ["#00ffb2", "#0072ff", "#a200ff", "#001a2c"], + distortion: 0.6, + swirl: 0.5, + speed: 0.3, + }, + }, + { + id: "mesh-citrus", + value: "mesh-gradient", + label: "Mesh — Citrus", + props: { + colors: ["#fff200", "#ff8a00", "#ff3d00", "#ffe08a"], + distortion: 0.7, + swirl: 0.4, + speed: 0.5, + }, + }, + { + id: "warp", + value: "warp", + label: "Warp", + props: { + colors: ["#121212", "#9470ff", "#121212", "#8838ff"], + speed: 0.4, + }, + }, + { + id: "waves", + value: "waves", + label: "Waves", + props: { colorFront: "#ffbb00", colorBack: "#000000" }, + }, + { + id: "voronoi", + value: "voronoi", + label: "Voronoi", + props: { colors: ["#ff8247", "#ffe53d"], speed: 0.3 }, + }, + { + id: "swirl", + value: "swirl", + label: "Swirl", + props: { + colorBack: "#180018", + colors: ["#ffd1d1", "#ff8a8a", "#660000"], + speed: 0.2, + }, + }, + { + id: "dot-orbit", + value: "dot-orbit", + label: "Orbit", + props: { + colors: ["#ffc96b", "#ff6200", "#ff2f00"], + colorBack: "#000000", + speed: 0.6, + }, + }, + { + id: "dot-grid", + value: "dot-grid", + label: "Grid", + props: { + colorBack: "#000000", + colorFill: "#ffffff", + colorStroke: "#ffaa00", + }, + }, + { + id: "smoke-ring", + value: "smoke-ring", + label: "Smoke", + props: { colorBack: "#000000", colors: ["#ffffff"], speed: 0.3 }, + }, + { + id: "static-radial-gradient", + value: "static-radial-gradient", + label: "Radial", + props: { colorBack: "#000000", colors: ["#00bbff", "#00ffe1", "#ffffff"] }, + }, + { + id: "neuro-noise", + value: "neuro-noise", + label: "Neuro", + props: { + colorFront: "#ffffff", + colorMid: "#47a6ff", + colorBack: "#000000", + speed: 0.4, + }, + }, + { + id: "water", + value: "water", + label: "Water", + props: { colorBack: "#909090", colorHighlight: "#ffffff", speed: 0.4 }, + }, + { + id: "metaballs", + value: "metaballs", + label: "Metaballs", + props: { + colors: ["#ff5cf4", "#4d9eff", "#000000"], + colorBack: "#000000", + speed: 0.5, + }, + }, + { + id: "god-rays", + value: "god-rays", + label: "Rays", + props: { + colors: ["#ffcc66", "#ff6a00"], + colorBack: "#000000", + speed: 0.4, + }, + }, + { + id: "spiral", + value: "spiral", + label: "Spiral", + props: { colors: ["#7a5cff", "#ff5ca6", "#000000"], speed: 0.4 }, + }, + { + id: "dithering", + value: "dithering", + label: "Dither", + props: { + colorBack: "#000000", + colorFront: "#00ff9d", + speed: 0.4, + }, + }, + { + id: "pulsing-border", + value: "pulsing-border", + label: "Pulse", + props: { + colors: ["#00e5ff", "#7000ff", "#ff00c8"], + colorBack: "#000000", + speed: 0.5, + }, + }, + { + id: "color-panels", + value: "color-panels", + label: "Panels", + props: { colors: ["#ff3d68", "#ffb800", "#3d7aff", "#00ffb2"], speed: 0.3 }, + }, + { + id: "static-mesh-gradient", + value: "static-mesh-gradient", + label: "Static Mesh", + props: { colors: ["#ff8a3d", "#ff3d9a", "#3d5aff", "#0a0a0a"] }, + }, + { + id: "static-mesh-dusk", + value: "static-mesh-gradient", + label: "Static Mesh — Dusk", + props: { colors: ["#2b1055", "#7597de", "#f6a1c8", "#0d0221"] }, + }, + { + id: "radial-ember", + value: "static-radial-gradient", + label: "Radial — Ember", + props: { colorBack: "#0a0000", colors: ["#ff5100", "#ffce00", "#4a0000"] }, + }, + { + id: "simplex-noise", + value: "simplex-noise", + label: "Simplex", + props: { colors: ["#ff6ec7", "#6ec7ff", "#000000"], speed: 0.4 }, + }, + { + id: "perlin-noise", + value: "perlin-noise", + label: "Perlin", + props: { colorFront: "#00ffd5", colorBack: "#000000", speed: 0.3 }, + }, +]; + +// Every variant carries a different prop shape; the switcher only ever spreads +// each entry's own preset, so the loose cast here is safe. +const Background = ShaderBackground as ComponentType< + ShaderBackgroundProps & Record +>; + +export function ShaderBackgroundPreview() { + const [active, setActive] = useState(VARIANTS[0].id); + const current = VARIANTS.find((v) => v.id === active) ?? VARIANTS[0]; + + return ( +
+
+ +
+ + + {VARIANTS.map((v) => ( + + {v.label} + + ))} + + +
+ ); +} diff --git a/lib/registry.ts b/lib/registry.ts index 41cb29a..24cde35 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -443,6 +443,22 @@ export const registry: CategoryEntry[] = [ }, ], }, + { + 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.", + file: "components/motion/shader-background.tsx", + badge: "new", + keywords: [ + "shader background react", + "webgl background", + "mesh gradient react", + "animated background react", + "canvas shader", + "gradient background component", + ], + }, ], }, { diff --git a/package.json b/package.json index b5ed4cd..6c4e81d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "check": "bun run typecheck && bun run lint && bun run check:registry" }, "dependencies": { + "@paper-design/shaders-react": "^0.0.76", "@shikijs/transformers": "^4.2.0", "@tanstack/react-virtual": "^3.14.4", "@vercel/analytics": "^1.4.1",