{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "background-controller", "title": "Background Controller", "description": "Site-wide animated background system — a provider + segmented switcher driving CSS gradient, JS canvas, and pure-CSS pattern backdrops (dots, grid, mesh, starfield, synthwave/moonrise horizons, Pac-Man chase, and more) that recolor with the live glass tint and accent. Exposes density, speed, accent, and a themed \"none\" base-color.", "dependencies": [ "@phosphor-icons/react", "clsx", "tailwind-merge" ], "registryDependencies": [ "@sistine/canvas-background-utils", "@sistine/oklch-utils", "@sistine/theme" ], "files": [ { "path": "lib/utils.ts", "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", "type": "registry:lib", "target": "lib/utils.ts" }, { "path": "components/pattern-background.tsx", "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Pure-CSS pattern backgrounds — tileable wallpapers built entirely from `background-image` layers\n * (multiple backgrounds + gradients) and SVG `mask-image` silhouettes, all colored off the live\n * OKLCH tint tokens so they recolor with the theme. No canvas, no rAF loop: each static style is a\n * single GPU paint; the animated scenes (synthwave / moonrise grid scroll, the Pac-Man chase) move\n * on compositor-only transforms. Sibling of GradientBackground (CSS gradient) and CanvasBackground\n * (JS canvas).\n */\n\nexport type PatternStyle = \"dots\" | \"grid\" | \"mesh\" | \"starfield\" | \"synthwave\" | \"moonrise\" | \"chase\";\n\nexport const PATTERN_STYLES: PatternStyle[] = [\n \"dots\",\n \"grid\",\n \"mesh\",\n \"starfield\",\n \"synthwave\",\n \"moonrise\",\n \"chase\",\n];\n\n/** Patterns that animate (CSS keyframes) — gates the speed control and shares the --pat-dur/--pat-play pace. */\nexport const ANIMATED_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n \"chase\",\n]);\n\n/** Horizon scenes with a sun/moon disc that can be placed left / center / right. */\nexport const DISC_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n]);\n\n/** Star-field density — how many of the star field's points render (a \"range of stars\"). */\nexport type PatternDensity = \"sparse\" | \"medium\" | \"dense\";\nexport const PATTERN_DENSITIES: PatternDensity[] = [\n \"sparse\",\n \"medium\",\n \"dense\",\n];\nconst STAR_COUNT: Record = {\n sparse: 16,\n medium: 33,\n dense: 50,\n};\n/** Chase pellet-cell scale per density — sparse = larger cells (fewer per screen), dense = smaller (more). */\nconst DENSITY_SCALE: Record = {\n sparse: 1.4,\n medium: 1,\n dense: 0.68,\n};\n/** Tile-size scale for the geometric tiles (dots / grid) — a much airier ladder: the old sparse (1.4)\n * is the new DENSE, with medium and sparse extrapolated outward on the same ~1.4× step. */\nconst TILE_SCALE: Record = {\n sparse: 2.8,\n medium: 2,\n dense: 1.4,\n};\n\n// SVG silhouettes as mask sources. White fill + transparent ground works whether the browser samples\n// the mask by alpha or luminance. Wrapped in url(...) after encoding so it's a valid CSS value.\nconst svgMask = (body: string, viewBox = \"0 0 100 100\") =>\n `url(\"data:image/svg+xml,${encodeURIComponent(`${body}`)}\")`;\n\n// The ghost silhouette — used by the chase scene's fleeing ghosts.\nconst GHOST = svgMask(\n \"\",\n);\n\n// Scattered \"points\" for the galaxy — a fixed set so it's deterministic (SSR-safe, no Math.random).\n// Kept as an array so the density control can render just a slice of them.\nconst STAR_GRADIENTS = [\n [\n 8,\n 12,\n ],\n [\n 22,\n 64,\n ],\n [\n 15,\n 88,\n ],\n [\n 35,\n 26,\n ],\n [\n 44,\n 72,\n ],\n [\n 52,\n 16,\n ],\n [\n 61,\n 52,\n ],\n [\n 68,\n 84,\n ],\n [\n 73,\n 34,\n ],\n [\n 82,\n 66,\n ],\n [\n 88,\n 20,\n ],\n [\n 92,\n 80,\n ],\n [\n 28,\n 44,\n ],\n [\n 57,\n 90,\n ],\n [\n 4,\n 40,\n ],\n [\n 12,\n 6,\n ],\n [\n 19,\n 30,\n ],\n [\n 26,\n 78,\n ],\n [\n 33,\n 54,\n ],\n [\n 40,\n 20,\n ],\n [\n 47,\n 46,\n ],\n [\n 54,\n 68,\n ],\n [\n 63,\n 12,\n ],\n [\n 66,\n 40,\n ],\n [\n 70,\n 60,\n ],\n [\n 77,\n 78,\n ],\n [\n 80,\n 8,\n ],\n [\n 84,\n 48,\n ],\n [\n 90,\n 60,\n ],\n [\n 95,\n 34,\n ],\n [\n 6,\n 70,\n ],\n [\n 48,\n 4,\n ],\n [\n 58,\n 30,\n ],\n [\n 76,\n 22,\n ],\n [\n 2,\n 22,\n ],\n [\n 16,\n 50,\n ],\n [\n 24,\n 14,\n ],\n [\n 38,\n 88,\n ],\n [\n 50,\n 40,\n ],\n [\n 56,\n 58,\n ],\n [\n 64,\n 74,\n ],\n [\n 72,\n 46,\n ],\n [\n 86,\n 38,\n ],\n [\n 94,\n 12,\n ],\n [\n 10,\n 82,\n ],\n [\n 30,\n 68,\n ],\n [\n 42,\n 34,\n ],\n [\n 60,\n 6,\n ],\n [\n 74,\n 90,\n ],\n [\n 96,\n 66,\n ],\n].map(([x, y], i) => {\n // a range of stars — occasional bright glints, most fine\n const r = i % 7 === 0 ? \"2px\" : i % 4 === 0 ? \"1.6px\" : i % 3 === 0 ? \"1.3px\" : \"1px\";\n return `radial-gradient(${r} ${r} at ${x}% ${y}%, #fff, transparent)`;\n});\n\n// Easter egg: the \"Muse\" — the Agent mascot's head (fedora + visored helmet) traced as a sparse star\n// constellation in every star field (starfield / synthwave / moonrise). The dots are anchor points sampled\n// from the mascot SVG's bezier paths, cropped to the head (the coat sprawled into unreadable scatter),\n// thinned to ~45 (downsample, dedupe) and re-normalized; rendered as stars only — no fill, no lines — so the\n// fedora-and-helmet silhouette hides among the real stars until you catch it. Hue follows the accent-or-tint.\n// biome-ignore format: keep the sampled dots as one compact \"x,y x,y …\" string (not a 400-line array).\nconst MUSE_DOTS = \"67,66.5 65.1,52.1 66.7,42.3 75.8,38.4 11,48.8 82.9,45.3 92.3,42.7 100,36.8 84.4,32.8 85.2,58.8 75.5,65.3 71.9,58.3 51.9,42.9 26.4,48.3 60.9,59.3 38.5,42.3 0,50.4 18.3,38 19.3,28.2 19.9,14.7 25.7,5.9 37.5,2.8 51.1,0 62.9,0.5 72.8,5.6 77.5,15.3 79.7,25.7 96.2,90.9 67,78 43.4,85.6 87.4,72.2 87.4,82.7 16.9,65.6 48.2,59.2 37.5,62 28.1,64 19,87 27.2,88.7 88.4,92.4 38.6,70.3 51.5,81.1 21.5,73 33.6,81 53.7,65.1 30.7,40.5\".split(\" \").map((p) => p.split(\",\").map(Number));\nconst MUSE_STYLES = `\n.star-muse {\n position: absolute;\n left: 6%;\n top: 3%;\n width: 58vh;\n height: 53.6vh;\n pointer-events: none;\n --muse-h: var(--accent-h, var(--glass-tint-h));\n filter: drop-shadow(0 0 1px oklch(0.72 0.17 var(--muse-h) / 0.5));\n /* Rides the star gate: hidden with the stars in the Tron scenes' day mode; DIMMED (not hidden) with\n them in starfield's day mode. */\n opacity: calc(0.9 * var(--sw-stars-o, 1));\n}\n.star-muse circle {\n fill: oklch(0.96 0.04 var(--muse-h));\n}`;\n\nfunction MuseConstellation() {\n return (\n <>\n \n \n {MUSE_DOTS.map(([x, y], i) => (\n \n ))}\n \n \n );\n}\n\n// Synthwave / Tron horizon — two variants: `synthwave` (outrun sun) and `moonrise` (purple-blue moon),\n// each with a DAY and a NIGHT palette so the scene follows the light/dark toggle. Every scene color is a\n// --sw-* custom property: day values sit on [data-pattern=\"…\"], night values on `.dark [data-pattern=\"…\"]`\n// (the injected \n
\n
\n {PAC_WALLS.map((w) => (\n \n ))}\n {PAC_LANES.map((lane) => (\n \n
\n
\n
\n
\n
\n
\n
\n
\n ))}\n {PAC_VLANES.map((lane) => (\n \n
\n
\n
\n
\n
\n
\n
\n
\n ))}\n
\n
\n );\n }\n if (style === \"synthwave\" || style === \"moonrise\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n const moon = style === \"moonrise\";\n return (\n \n \n
\n \n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n );\n }\n if (style === \"starfield\") {\n // Scene colors live in STARFIELD_STYLES' palette blocks (an inline background would beat the .dark\n // day/night flip); only the star GLINTS — white in both modes — are painted inline, on their own\n // layer so the palette's --sw-stars-o can dim them by day.\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n
\n \n
\n \n \n
\n );\n }\n return (\n \n );\n}\n", "type": "registry:component", "target": "components/pattern-background.tsx" }, { "path": "components/canvas-background.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { backdropPalette, type CanvasRamp, type CanvasStyle, createCanvas, useBackdropTint } from \"@/lib/canvas-background-utils\";\n\ninterface CanvasBackgroundProps {\n /** Canvas style: gradient | lava | circle. */\n style?: CanvasStyle;\n /** Hue override (deg). When set, drives the canvas color instead of the live --glass-tint-h. */\n hue?: number;\n /** Ramp axis the colors follow (lightness = \"linear\"). */\n ramp?: CanvasRamp;\n /** Steps per side (4–12). */\n steps?: number;\n /** Gradient angle in degrees (gradient style). */\n angle?: number;\n /** Animation pace (0 = static). */\n speed?: number;\n /** Whether to animate. */\n animated?: boolean;\n /** Seed for deterministic placement. */\n seed?: string;\n className?: string;\n /** Opacity of the background (0–1). */\n opacity?: number;\n /** Blur the canvas. */\n blur?: boolean;\n}\n\nexport function CanvasBackground({\n style: canvasStyle = \"gradient\",\n hue,\n ramp = \"tonal\",\n steps = 6,\n angle = 90,\n speed = 1,\n animated = false,\n seed,\n className = \"\",\n opacity = 1,\n blur = false,\n}: CanvasBackgroundProps) {\n const canvasRef = React.useRef(null);\n const frameRef = React.useRef(undefined);\n const [dimensions, setDimensions] = React.useState({\n width: 1920,\n height: 1080,\n dpr: 1,\n });\n // Live base color tracks the glass tint, so the canvas recolors with the theme (shared with the\n // CSS gradient background).\n const tint = useBackdropTint();\n\n React.useEffect(() => {\n const update = () => {\n // Back the canvas at device resolution (capped 2×) so it isn't upscaled/blurry on HiDPI.\n const dpr = Math.min(2, window.devicePixelRatio || 1);\n setDimensions({\n width: Math.round(window.innerWidth * dpr),\n height: Math.round(window.innerHeight * dpr),\n dpr,\n });\n };\n update();\n window.addEventListener(\"resize\", update);\n return () => window.removeEventListener(\"resize\", update);\n }, []);\n\n React.useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext(\"2d\", {\n colorSpace: \"display-p3\",\n });\n if (!ctx) return;\n canvas.width = dimensions.width;\n canvas.height = dimensions.height;\n\n // Shared with GradientBackground: theme-tracking base + fresco palette (`hue` prop overrides both).\n const { base, frescoColors } = backdropPalette(tint, hue);\n const { step } = createCanvas({\n width: dimensions.width,\n height: dimensions.height,\n dpr: dimensions.dpr,\n color: base,\n colors: frescoColors,\n style: canvasStyle,\n ramp,\n steps,\n angle,\n speed,\n p3: tint.p3,\n seed: seed || window.location.pathname,\n });\n\n if (animated && speed > 0) {\n let last = 0;\n const paint = (now: number) => {\n frameRef.current = requestAnimationFrame(paint);\n // ~30fps cap — the gradient repaint / gooey blur are costly and the motion is slow.\n if (now - last < 33) return;\n last = now;\n step(ctx, now / 1000);\n };\n frameRef.current = requestAnimationFrame(paint);\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n step(ctx, 0);\n }\n\n return () => {\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n };\n }, [\n dimensions,\n tint,\n hue,\n canvasStyle,\n ramp,\n steps,\n angle,\n speed,\n animated,\n seed,\n ]);\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "components/canvas-background.tsx" }, { "path": "components/gradient-background.tsx", "content": "\"use client\";\n\nimport { backdropPalette, useBackdropTint } from \"@/lib/canvas-background-utils\";\nimport { type GradientGeometry, type GradientShape, type RampGradientAxis, rampGradient, wrapGradient } from \"@/lib/oklch-utils\";\n\n/**\n * A ramp-driven gradient wallpaper. The gradient is one of our oklch ramps (hue / lightness / tonal\n * / chroma) centered on the live glass-tint color (so it recolors with the theme) with the center as a\n * slightly wider plateau, painted as a `linear`, `radial`, or `conic` gradient. Pure CSS — crisp at any DPI.\n */\nexport function GradientBackground({\n axis = \"tonal\",\n hue,\n angle = 90,\n shape = \"linear\",\n position,\n radialShape,\n radialSize,\n}: {\n axis?: RampGradientAxis;\n /** Hue override (deg). When set, drives the gradient color instead of the live --glass-tint-h. */\n hue?: number;\n angle?: number;\n shape?: GradientShape;\n} & GradientGeometry) {\n const tint = useBackdropTint();\n // Shared with CanvasBackground: theme-tracking base + fresco palette (the `hue` prop overrides both).\n const { base, frescoColors } = backdropPalette(tint, hue);\n const gradient = frescoColors\n ? wrapGradient(shape, frescoColors.join(\", \"), {\n angle,\n position,\n radialShape,\n radialSize,\n })\n : rampGradient(axis, base, 5, {\n angle,\n gamut: tint.p3 ? \"p3\" : \"srgb\",\n shape,\n position,\n radialShape,\n radialSize,\n });\n\n return (\n \n {/* Subtle organic highlight/shadow for depth — plain alpha layers, NO mix-blend-mode: a\n blend-mode element on the page can disable backdrop-filter RENDERING in Chromium, killing\n every glass blur above it (computed styles stay correct, paint silently fails). */}\n \n
\n );\n}\n", "type": "registry:component", "target": "components/gradient-background.tsx" }, { "path": "components/background-provider.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CanvasBackground } from \"@/components/canvas-background\";\nimport { GradientBackground } from \"@/components/gradient-background\";\nimport { PATTERN_DENSITIES, PATTERN_STYLES, PatternBackground, type PatternDensity, type PatternStyle } from \"@/components/pattern-background\";\nimport type { CanvasStyle } from \"@/lib/canvas-background-utils\";\nimport type { GradientGeometry, GradientShape, RampGradientAxis } from \"@/lib/oklch-utils\";\n\nexport type BackgroundType = \"gradient\" | \"canvas\" | \"pattern\" | \"none\";\n\nconst STORAGE_KEY = \"sistine-background\";\n\n/** Canvas styles (lava renamed from \"blobs\", circle from \"circles\"). */\nexport const CANVAS_STYLES: CanvasStyle[] = [\n \"gradient\",\n \"lava\",\n \"circle\",\n];\n\n/** Ramp axes shared by the Gradient background and the Canvas (lightness = the \"linear\" ramp). */\nexport const RAMP_AXES: RampGradientAxis[] = [\n \"tonal\",\n \"hue\",\n \"lightness\",\n \"chroma\",\n];\n\n/** Gradient painting shapes the Gradient background cycles through. */\nexport const GRADIENT_SHAPES: GradientShape[] = [\n \"linear\",\n \"radial\",\n \"conic\",\n];\n\n/** Center positions (radial + conic) the switcher cycles through. */\nexport const GRADIENT_POSITIONS: {\n value: string;\n label: string;\n}[] = [\n {\n value: \"50% 50%\",\n label: \"center\",\n },\n {\n value: \"0% 0%\",\n label: \"top-left\",\n },\n {\n value: \"100% 0%\",\n label: \"top-right\",\n },\n {\n value: \"100% 100%\",\n label: \"bottom-right\",\n },\n {\n value: \"0% 100%\",\n label: \"bottom-left\",\n },\n];\n\n/** The position values the cycler steps through (labels live in GRADIENT_POSITIONS). */\nconst GRADIENT_POSITION_VALUES = GRADIENT_POSITIONS.map((p) => p.value);\n\n/** Radial shape + size options. */\nexport const RADIAL_SHAPES: NonNullable[] = [\n \"circle\",\n \"ellipse\",\n];\nexport const RADIAL_SIZES: NonNullable[] = [\n \"farthest-corner\",\n \"farthest-side\",\n \"closest-corner\",\n \"closest-side\",\n];\n\n/** Angle presets — 45° steps around the circle, shared by the gradient + canvas angle cyclers. */\nconst ANGLES = [\n 0,\n 45,\n 90,\n 135,\n 180,\n 225,\n 270,\n 315,\n];\n\n/** Steps-per-side presets the canvas/ramps cycle through (4–12). */\nexport const CANVAS_STEPS = [\n 4,\n 6,\n 8,\n 10,\n 12,\n] as const;\n\n/** Animation pace presets. */\nexport const CANVAS_SPEEDS = [\n 0.5,\n 1,\n 2,\n] as const;\n\n/** Animated-pattern pace: loop/scroll duration in SECONDS (2s fastest … 16s slowest); 0 = static.\n * Shared by every animated pattern (ghost, synthwave, moonrise, chase) via the --pat-dur CSS var. */\nexport const PATTERN_SPEEDS = [\n 2,\n 4,\n 8,\n 12,\n 16,\n 0,\n] as const;\n\n/** Sun/moon horizontal placement for the horizon scenes (synthwave / moonrise) — off-center clears the\n * page's centered content. */\nexport const PATTERN_DISCS = [\n \"right\",\n \"center\",\n \"left\",\n] as const;\n\nfunction persistBackground(next: BackgroundType) {\n try {\n localStorage.setItem(STORAGE_KEY, next);\n } catch {\n // ignore storage failures (private mode, etc.)\n }\n}\n\nfunction pickRandom(items: readonly T[]): T {\n return items[Math.floor(Math.random() * items.length)];\n}\n\nfunction cycle(items: readonly T[], current: T): T {\n const i = items.indexOf(current);\n return items[(i + 1) % items.length];\n}\n\n/**\n * State that steps through a fixed preset list, wrapping at the end. Returns the value, a stable\n * advance-to-the-next-preset callback, and the raw setter.\n */\nfunction useCycle(\n items: readonly T[],\n initial: T,\n): [\n T,\n () => void,\n React.Dispatch>,\n] {\n const [value, setValue] = React.useState(initial);\n const next = React.useCallback(\n () => setValue((v) => cycle(items, v)),\n [\n items,\n ],\n );\n return [\n value,\n next,\n setValue,\n ];\n}\n\ninterface BackgroundContextValue {\n background: BackgroundType;\n setBackground: (background: BackgroundType) => void;\n /** The ramp axis driving the gradient background. */\n gradientAxis: RampGradientAxis;\n /** Switch to the gradient background and set its ramp axis. */\n setGradientAxis: (axis: RampGradientAxis) => void;\n /** Gradient angle in degrees (90 = left → right). */\n gradientAngle: number;\n /** Rotate the gradient by 45°. */\n cycleGradientAngle: () => void;\n /** The gradient painting shape (linear | radial | conic). */\n gradientShape: GradientShape;\n /** Advance to the next gradient shape. */\n cycleGradientShape: () => void;\n /** Center position for radial + conic gradients (CSS `at `). */\n gradientPosition: string;\n /** Advance to the next center-position preset. */\n cycleGradientPosition: () => void;\n /** Radial shape (circle | ellipse). */\n radialShape: NonNullable;\n /** Toggle the radial shape. */\n cycleRadialShape: () => void;\n /** Radial size keyword. */\n radialSize: NonNullable;\n /** Advance to the next radial size. */\n cycleRadialSize: () => void;\n // ── Canvas ──\n /** The canvas style (gradient | lava | circle). */\n canvasStyle: CanvasStyle;\n /** Switch to the canvas background and set its style. */\n setCanvasStyle: (style: CanvasStyle) => void;\n /** The ramp axis driving the canvas colors. */\n canvasRamp: RampGradientAxis;\n /** Switch to the canvas background and set its ramp axis. */\n setCanvasRamp: (axis: RampGradientAxis) => void;\n /** Steps per side (4–12). */\n canvasSteps: number;\n /** Advance to the next steps-per-side preset. */\n cycleCanvasSteps: () => void;\n /** Canvas gradient angle in degrees. */\n canvasAngle: number;\n /** Rotate the canvas gradient by 45°. */\n cycleCanvasAngle: () => void;\n /** Canvas animation pace. */\n canvasSpeed: number;\n /** Advance to the next animation-pace preset. */\n cycleCanvasSpeed: () => void;\n /** Whether the canvas is currently animating. */\n canvasAnimated: boolean;\n /** Toggle canvas animation on/off. */\n toggleCanvasAnimated: () => void;\n /** Switch to the canvas background and reshuffle it (new style + ramp + layout seed). */\n shuffleCanvas: () => void;\n // ── Pattern (pure-CSS wallpapers) ──\n /** The active CSS pattern style. */\n patternStyle: PatternStyle;\n /** Switch to the pattern background and set its style. */\n setPatternStyle: (style: PatternStyle) => void;\n /** Advance to the next pattern style. */\n cyclePatternStyle: () => void;\n /** Star-field density (sparse | medium | dense). */\n patternDensity: PatternDensity;\n /** Advance to the next star-field density. */\n cyclePatternDensity: () => void;\n /** Horizon-grid scroll pace (synthwave / moonrise); 0 is static. */\n patternSpeed: number;\n /** Advance to the next grid scroll pace (… → static). */\n cyclePatternSpeed: () => void;\n /** Sun/moon placement for the horizon scenes (right | center | left). */\n patternDisc: (typeof PATTERN_DISCS)[number];\n /** Advance to the next sun/moon placement. */\n cyclePatternDisc: () => void;\n // ── Base color (the \"none\" backdrop) ──\n /** Base color for the \"none\" backdrop; null = follow the theme (tint-tinted, light/dark aware). */\n baseColor: string | null;\n /** Override the \"none\" backdrop color; null clears back to the themed default. */\n setBaseColor: (color: string | null) => void;\n}\n\nconst BackgroundContext = React.createContext(null);\n\nexport function useBackground() {\n const context = React.useContext(BackgroundContext);\n if (!context) {\n throw new Error(\"useBackground must be used within a BackgroundProvider\");\n }\n return context;\n}\n\ninterface RenderArgs {\n gradientAxis: RampGradientAxis;\n gradientAngle: number;\n gradientShape: GradientShape;\n gradientPosition: string;\n radialShape: NonNullable;\n radialSize: NonNullable;\n canvasStyle: CanvasStyle;\n canvasRamp: RampGradientAxis;\n canvasSteps: number;\n canvasAngle: number;\n canvasSpeed: number;\n canvasAnimated: boolean;\n canvasSeed: string;\n patternStyle: PatternStyle;\n patternDensity: PatternDensity;\n patternSpeed: number;\n patternDisc: (typeof PATTERN_DISCS)[number];\n baseColor: string | null;\n}\n\nfunction renderBackground(background: BackgroundType, args: RenderArgs) {\n switch (background) {\n case \"canvas\":\n // key forces a clean redraw when the style/ramp/steps/seed/animation change\n return (\n \n );\n case \"pattern\":\n return ;\n case \"none\":\n // Forgo the decorative backdrop — a clean solid that honors the theme (tint hue, light/dark) by\n // default, overridable by the user's chosen base color.\n return (\n <>\n \n \n \n );\n default:\n return (\n \n );\n }\n}\n\n/**\n * Holds the site-wide background choice (persisted to localStorage) and renders it\n * behind the app. Pair with to preview + tune each style.\n */\nexport function BackgroundProvider({ children }: { children: React.ReactNode }) {\n // SSR + first client render use \"gradient\" so hydration matches; localStorage is read after mount.\n const [background, setBackgroundState] = React.useState(\"pattern\");\n const [gradientAxis, setGradientAxisState] = React.useState(\"tonal\");\n const [gradientAngle, cycleGradientAngle] = useCycle(ANGLES, 90);\n const [gradientShape, cycleGradientShape] = useCycle(GRADIENT_SHAPES, \"linear\");\n const [gradientPosition, cycleGradientPosition] = useCycle(GRADIENT_POSITION_VALUES, \"50% 50%\");\n const [radialShape, cycleRadialShape] = useCycle(RADIAL_SHAPES, \"circle\");\n const [radialSize, cycleRadialSize] = useCycle(RADIAL_SIZES, \"farthest-corner\");\n const [canvasStyle, setCanvasStyleState] = React.useState(\"gradient\");\n const [canvasRamp, setCanvasRampState] = React.useState(\"tonal\");\n const [canvasSteps, cycleCanvasSteps] = useCycle(CANVAS_STEPS, 6);\n const [canvasAngle, cycleCanvasAngle] = useCycle(ANGLES, 90);\n const [canvasSpeed, cycleCanvasSpeed] = useCycle(CANVAS_SPEEDS, 1);\n const [canvasAnimated, setCanvasAnimated] = React.useState(false);\n const [canvasSeed, setCanvasSeed] = React.useState(\"sistine\");\n const [patternStyle, cyclePatternStyleState, setPatternStyleState] = useCycle(PATTERN_STYLES, \"moonrise\");\n const [patternDensity, cyclePatternDensityState] = useCycle(PATTERN_DENSITIES, \"medium\");\n const [patternSpeed, cyclePatternSpeedState] = useCycle(PATTERN_SPEEDS, 8);\n const [baseColor, setBaseColor] = React.useState(null);\n const [patternDisc, cyclePatternDiscState] = useCycle(PATTERN_DISCS, \"right\");\n\n React.useEffect(() => {\n const stored = localStorage.getItem(STORAGE_KEY);\n if (stored === \"gradient\" || stored === \"canvas\" || stored === \"pattern\" || stored === \"none\") {\n setBackgroundState(stored);\n }\n }, []);\n\n /** The coupling every per-style setter/cycler shares: switch to that background type, apply, persist. */\n const withType =\n (type: BackgroundType, fn: (...args: A) => void) =>\n (...args: A) => {\n setBackgroundState(type);\n fn(...args);\n persistBackground(type);\n };\n\n const value: BackgroundContextValue = {\n background,\n setBackground: (next) => {\n setBackgroundState(next);\n persistBackground(next);\n },\n gradientAxis,\n setGradientAxis: withType(\"gradient\", setGradientAxisState),\n gradientAngle,\n cycleGradientAngle,\n gradientShape,\n cycleGradientShape,\n gradientPosition,\n cycleGradientPosition,\n radialShape,\n cycleRadialShape,\n radialSize,\n cycleRadialSize,\n canvasStyle,\n setCanvasStyle: withType(\"canvas\", setCanvasStyleState),\n canvasRamp,\n setCanvasRamp: withType(\"canvas\", setCanvasRampState),\n canvasSteps,\n cycleCanvasSteps,\n canvasAngle,\n cycleCanvasAngle,\n canvasSpeed,\n cycleCanvasSpeed,\n canvasAnimated,\n toggleCanvasAnimated: () => setCanvasAnimated((on) => !on),\n shuffleCanvas: withType(\"canvas\", () => {\n setCanvasStyleState(pickRandom(CANVAS_STYLES));\n setCanvasRampState(pickRandom(RAMP_AXES));\n setCanvasSeed(Math.random().toString(36).slice(2));\n }),\n patternStyle,\n setPatternStyle: withType(\"pattern\", setPatternStyleState),\n cyclePatternStyle: withType(\"pattern\", cyclePatternStyleState),\n patternDensity,\n cyclePatternDensity: withType(\"pattern\", cyclePatternDensityState),\n patternSpeed,\n cyclePatternSpeed: withType(\"pattern\", cyclePatternSpeedState),\n patternDisc,\n cyclePatternDisc: withType(\"pattern\", cyclePatternDiscState),\n baseColor,\n setBaseColor,\n };\n\n return (\n \n {renderBackground(background, {\n gradientAxis,\n gradientAngle,\n gradientShape,\n gradientPosition,\n radialShape,\n radialSize,\n canvasStyle,\n canvasRamp,\n canvasSteps,\n canvasAngle,\n canvasSpeed,\n canvasAnimated,\n canvasSeed,\n patternStyle,\n patternDensity,\n patternSpeed,\n patternDisc,\n baseColor,\n })}\n {children}\n \n );\n}\n", "type": "registry:component", "target": "components/background-provider.tsx" }, { "path": "components/background-switcher.tsx", "content": "\"use client\";\n\nimport { PaletteIcon, PauseIcon, PlayIcon, ProhibitIcon, ShuffleIcon, SparkleIcon, SquaresFourIcon } from \"@phosphor-icons/react\";\nimport { type BackgroundType, CANVAS_STYLES, RAMP_AXES, useBackground } from \"@/components/background-provider\";\nimport { ANIMATED_PATTERNS, DISC_PATTERNS, PATTERN_STYLES, type PatternStyle } from \"@/components/pattern-background\";\nimport type { RampGradientAxis } from \"@/lib/oklch-utils\";\nimport { cn } from \"@/lib/utils\";\n\nconst options: {\n value: BackgroundType;\n label: string;\n Icon: typeof PaletteIcon;\n}[] = [\n {\n value: \"gradient\",\n label: \"Gradient\",\n Icon: PaletteIcon,\n },\n {\n value: \"canvas\",\n label: \"Canvas\",\n Icon: SparkleIcon,\n },\n {\n value: \"pattern\",\n label: \"Pattern\",\n Icon: SquaresFourIcon,\n },\n {\n value: \"none\",\n label: \"None\",\n Icon: ProhibitIcon,\n },\n];\n\n/** Compact glyphs for the gradient center + radial size. */\nconst POS_SYMBOL: Record = {\n \"50% 50%\": \"•\",\n \"0% 0%\": \"↖\",\n \"100% 0%\": \"↗\",\n \"100% 100%\": \"↘\",\n \"0% 100%\": \"↙\",\n};\nconst SIZE_SHORT: Record = {\n \"farthest-corner\": \"f-cnr\",\n \"farthest-side\": \"f-side\",\n \"closest-corner\": \"c-cnr\",\n \"closest-side\": \"c-side\",\n};\n\nconst segmentButton = \"inline-flex h-7 items-center justify-center rounded-md transition-colors\";\nconst active = \"bg-foreground/10 text-foreground\";\nconst idle = \"text-muted-foreground hover:bg-foreground/5 hover:text-foreground\";\n\n/** The ramp-axis button group (T / H / L / C) shared by the Gradient and Canvas panels. */\nfunction AxisPicker({\n value,\n onSelect,\n titleFor,\n labelFor,\n}: {\n value: RampGradientAxis;\n onSelect: (axis: RampGradientAxis) => void;\n titleFor: (axis: RampGradientAxis) => string;\n labelFor: (axis: RampGradientAxis) => string;\n}) {\n return (\n <>\n {RAMP_AXES.map((axis) => (\n onSelect(axis)}\n className={cn(segmentButton, \"w-7 text-[11px] font-semibold uppercase\", value === axis ? active : idle)}\n >\n {axis[0]}\n \n ))}\n \n );\n}\n\n/**\n * Compact segmented control to switch the site-wide background. Gradient exposes a ramp-axis +\n * angle picker; Canvas exposes style (gradient / lava / circle), ramp axis, steps-per-side, angle,\n * speed, shuffle, and animate. Great for previewing how glass components read against each backdrop.\n */\nexport function BackgroundSwitcher() {\n const {\n background,\n setBackground,\n gradientAxis,\n setGradientAxis,\n gradientAngle,\n cycleGradientAngle,\n gradientShape,\n cycleGradientShape,\n gradientPosition,\n cycleGradientPosition,\n radialShape,\n cycleRadialShape,\n radialSize,\n cycleRadialSize,\n canvasStyle,\n setCanvasStyle,\n canvasRamp,\n setCanvasRamp,\n canvasSteps,\n cycleCanvasSteps,\n canvasAngle,\n cycleCanvasAngle,\n canvasSpeed,\n cycleCanvasSpeed,\n canvasAnimated,\n toggleCanvasAnimated,\n shuffleCanvas,\n patternStyle,\n setPatternStyle,\n patternDensity,\n cyclePatternDensity,\n patternSpeed,\n cyclePatternSpeed,\n patternDisc,\n cyclePatternDisc,\n baseColor,\n setBaseColor,\n } = useBackground();\n\n const nextStyle = CANVAS_STYLES[(CANVAS_STYLES.indexOf(canvasStyle) + 1) % CANVAS_STYLES.length];\n\n return (\n \n {options.map(({ value, label, Icon }) => (\n setBackground(value)}\n className={cn(segmentButton, \"w-7\", background === value ? active : idle)}\n >\n \n \n ))}\n\n {background === \"gradient\" && (\n
\n `${axis} gradient`}\n labelFor={(axis) => `${axis} gradient`}\n />\n {/* Shape: linear | radial | conic */}\n \n {gradientShape}\n \n {/* Center position — radial + conic */}\n {(gradientShape === \"radial\" || gradientShape === \"conic\") && (\n \n {POS_SYMBOL[gradientPosition] ?? \"•\"}\n \n )}\n {/* Radial shape + size */}\n {gradientShape === \"radial\" && (\n <>\n \n {radialShape}\n \n \n {SIZE_SHORT[radialSize] ?? radialSize}\n \n \n )}\n {/* Angle — not meaningful for radial (fixed center) */}\n {gradientShape !== \"radial\" && (\n \n {gradientAngle}°\n \n )}\n
\n )}\n\n {background === \"canvas\" && (\n
\n {/* Style: gradient | lava | circle */}\n setCanvasStyle(nextStyle)}\n className={cn(segmentButton, \"px-1.5 text-[10px] font-semibold capitalize\", active)}\n >\n {canvasStyle}\n \n {/* Ramp axis (lightness = \"linear\") */}\n `${axis === \"lightness\" ? \"linear (lightness)\" : axis} ramp`}\n labelFor={(axis) => `${axis} ramp`}\n />\n {/* Steps per side (4–12) */}\n \n {canvasSteps}×\n \n {/* Angle — gradient style only */}\n {canvasStyle === \"gradient\" && (\n \n {canvasAngle}°\n \n )}\n {/* Speed — only meaningful while animating */}\n {canvasAnimated && (\n \n {canvasSpeed}×\n \n )}\n \n \n \n \n {canvasAnimated ? : }\n \n
\n )}\n\n {background === \"pattern\" && (\n
\n {/* Direct pick — jump straight to any pattern instead of cycling through all of them. */}\n setPatternStyle(e.target.value as PatternStyle)}\n title=\"Pattern — pick one\"\n aria-label=\"Pattern style\"\n className={cn(segmentButton, \"cursor-pointer bg-transparent px-1.5 text-[10px] font-semibold capitalize outline-none\", active)}\n >\n {PATTERN_STYLES.map((s) => (\n \n ))}\n \n \n {patternDensity}\n \n {ANIMATED_PATTERNS.has(patternStyle) && (\n \n {patternSpeed === 0 ? \"Static\" : `${patternSpeed}s`}\n \n )}\n {DISC_PATTERNS.has(patternStyle) && (\n \n {patternDisc[0]}\n \n )}\n
\n )}\n\n {background === \"none\" && (\n
\n \n Base color\n \n setBaseColor(e.target.value)}\n className=\"absolute inset-0 cursor-pointer opacity-0\"\n />\n \n {baseColor && (\n setBaseColor(null)}\n className={cn(segmentButton, \"px-1.5 text-[10px] font-semibold\", idle)}\n >\n Theme\n \n )}\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/background-switcher.tsx" } ], "type": "registry:block" }