Workspace / component / backgrounds

Bokeh Background

Out-of-focus light discs at three depths drifting on independent axes. Depth is faked honestly with size, blur, and parallax speed.

Workspacecomponentbackgrounds

paxfx add blocks/component-backgrounds-bokehlocal only

Tier: medium. Reduced motion: Discs render in fixed positions.

"use client";

import type { CSSProperties, ReactNode } from "react";
import { cn, useReducedMotion } from "@pax-sh/blocks";

export interface BokehBackgroundProps {
  /** 0 to 1; overall visibility of the light discs. */
  intensity?: number;
  /** Multiplier on drift speed. 1 is the tuned default. */
  speed?: number;
  className?: string;
  children?: ReactNode;
}

interface Disc {
  x: number;
  y: number;
  size: number;
  tone: "acid" | "warm" | "mute";
}

const TONES: Record<Disc["tone"], string> = {
  acid: "color-mix(in srgb, var(--pax-acid,#c8ff00) 70%, transparent)",
  warm: "rgba(255,244,214,0.9)",
  mute: "color-mix(in srgb, var(--pax-mute,#6b645c) 40%, transparent)",
};

const FAR: Disc[] = [
  { x: 12, y: 22, size: 48, tone: "warm" },
  { x: 34, y: 70, size: 40, tone: "acid" },
  { x: 58, y: 16, size: 56, tone: "mute" },
  { x: 76, y: 62, size: 44, tone: "warm" },
  { x: 90, y: 30, size: 52, tone: "acid" },
  { x: 22, y: 88, size: 46, tone: "mute" },
];

const MID: Disc[] = [
  { x: 20, y: 44, size: 104, tone: "acid" },
  { x: 50, y: 78, size: 92, tone: "warm" },
  { x: 68, y: 34, size: 118, tone: "mute" },
  { x: 88, y: 74, size: 96, tone: "acid" },
];

const NEAR: Disc[] = [
  { x: 8, y: 74, size: 210, tone: "warm" },
  { x: 52, y: 10, size: 184, tone: "acid" },
  { x: 86, y: 52, size: 232, tone: "mute" },
];

/**
 * Out-of-focus light discs at three depths drifting on independent axes.
 * Depth is faked honestly: far discs are small, sharp-ish, and slow; near
 * discs are large, heavily blurred, and drift furthest. Positions are a
 * fixed deterministic set. Under reduced motion every disc holds its place.
 */
export function BokehBackground({
  intensity = 0.8,
  speed = 1,
  className,
  children,
}: BokehBackgroundProps) {
  const reduced = useReducedMotion();
  const clamped = Math.min(1, Math.max(0, intensity));
  const dur = (seconds: number) => `${seconds / Math.max(0.1, speed)}s`;

  const depth = (discs: Disc[], blur: number, opacity: number, animation: string) => (
    <div
      aria-hidden="true"
      className="pointer-events-none absolute -inset-[10%] will-change-transform"
      style={{ animation: reduced ? "none" : animation }}
    >
      {discs.map((disc, i) => {
        const style: CSSProperties = {
          left: `${disc.x}%`,
          top: `${disc.y}%`,
          width: disc.size,
          height: disc.size,
          background: `radial-gradient(circle, ${TONES[disc.tone]} 0%, transparent 72%)`,
          filter: `blur(${blur}px)`,
          opacity: opacity * clamped,
        };
        return (
          <div
            key={i}
            className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full"
            style={style}
          />
        );
      })}
    </div>
  );

  return (
    <div
      className={cn(
        "relative isolate overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))]",
        "bg-[var(--pax-paper,#f6f2ea)] text-[var(--pax-ink,#12110f)]",
        className,
      )}
      data-pax-asset="component-backgrounds-bokeh"
    >
      <style>{`
        @keyframes pax-bokeh-drift-far {
          from { transform: translate3d(-1.5%, 1%, 0); }
          to { transform: translate3d(1.5%, -1%, 0); }
        }
        @keyframes pax-bokeh-drift-mid {
          from { transform: translate3d(2%, 1.5%, 0); }
          to { transform: translate3d(-2%, -1.5%, 0); }
        }
        @keyframes pax-bokeh-drift-near {
          from { transform: translate3d(-3%, -2%, 0); }
          to { transform: translate3d(3%, 2%, 0); }
        }
      `}</style>
      {depth(FAR, 6, 0.7, `pax-bokeh-drift-far ${dur(36)} ease-in-out infinite alternate`)}
      {depth(MID, 14, 0.65, `pax-bokeh-drift-mid ${dur(28)} ease-in-out infinite alternate`)}
      {depth(NEAR, 30, 0.6, `pax-bokeh-drift-near ${dur(22)} ease-in-out infinite alternate`)}
      <div className="relative z-[1]">{children}</div>
    </div>
  );
}