{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-matrix-background",
  "type": "registry:ui",
  "title": "DotMatrixBackground",
  "description": "Animated landing-page background: a token-colored dot grid with seeded brightness pulses or an optional pointer-following local glow. Motion-powered, SSR-deterministic, offscreen-paused, with a designed reduced-motion static fallback.",
  "dependencies": [
    "motion@^12.43.0"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/dot-matrix-background/dot-matrix-background.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/dot-matrix-background/dot-matrix-background.tsx",
      "content": "import {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ForwardedRef,\n  type HTMLAttributes,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  MotionConfig,\n  motion as motionElement,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  type Transition,\n} from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { usePageVisible } from \"../../utils/use-page-visible\";\nimport { mulberry32, seededRange } from \"../../utils/seeded-random\";\n\nexport type DotMatrixBackgroundDensity = \"sparse\" | \"normal\" | \"dense\";\nexport type DotMatrixBackgroundIntensity = \"faint\" | \"subtle\" | \"bold\";\nexport type DotMatrixBackgroundMode = \"pulse\" | \"follow\";\nexport type DotMatrixBackgroundSpeed = \"slow\" | \"normal\" | \"fast\";\nexport type DotMatrixBackgroundTone = \"foreground\" | \"muted\" | \"primary\";\n\ntype MotionBackedEventProps =\n  | \"onAnimationEnd\"\n  | \"onAnimationEndCapture\"\n  | \"onAnimationIteration\"\n  | \"onAnimationIterationCapture\"\n  | \"onAnimationStart\"\n  | \"onAnimationStartCapture\"\n  | \"onDrag\"\n  | \"onDragCapture\"\n  | \"onDragEnd\"\n  | \"onDragEndCapture\"\n  | \"onDragEnter\"\n  | \"onDragEnterCapture\"\n  | \"onDragExit\"\n  | \"onDragExitCapture\"\n  | \"onDragLeave\"\n  | \"onDragLeaveCapture\"\n  | \"onDragOver\"\n  | \"onDragOverCapture\"\n  | \"onDragStart\"\n  | \"onDragStartCapture\";\n\ntype MotionSafeDivProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  MotionBackedEventProps\n>;\n\nexport interface DotMatrixBackgroundProps extends MotionSafeDivProps {\n  animate?: boolean;\n  children?: ReactNode;\n  density?: DotMatrixBackgroundDensity;\n  intensity?: DotMatrixBackgroundIntensity;\n  interactive?: boolean;\n  mode?: DotMatrixBackgroundMode;\n  seed?: number;\n  speed?: DotMatrixBackgroundSpeed;\n  tone?: DotMatrixBackgroundTone;\n}\n\nexport interface DotMatrixBackgroundMotionConfig {\n  animateEnabled: boolean;\n  pulseDuration: number;\n  reducedMotion: boolean;\n  transition: Transition;\n}\n\nexport interface DotMatrixBackgroundPulse {\n  delay: number;\n  originX: number;\n  originY: number;\n  repeatDelay: number;\n}\n\nconst dotMatrixBackgroundSpeedDurations: Record<\n  DotMatrixBackgroundSpeed,\n  number\n> = {\n  slow: 7,\n  normal: 5,\n  fast: 3,\n};\n\nexport function getDotMatrixBackgroundMotionConfig(\n  speed: DotMatrixBackgroundSpeed,\n  reducedMotion = false,\n): DotMatrixBackgroundMotionConfig {\n  if (reducedMotion) {\n    return {\n      animateEnabled: false,\n      pulseDuration: 0,\n      reducedMotion: true,\n      transition: { duration: 0 },\n    };\n  }\n\n  const pulseDuration = dotMatrixBackgroundSpeedDurations[speed];\n\n  return {\n    animateEnabled: true,\n    pulseDuration,\n    reducedMotion: false,\n    transition: {\n      duration: pulseDuration,\n      ease: \"easeInOut\",\n      repeat: Infinity,\n    },\n  };\n}\n\nconst dotMatrixBackgroundPulseCount = 3;\n\nfunction roundTo(value: number, decimals: number): number {\n  const factor = 10 ** decimals;\n  return Math.round(value * factor) / factor;\n}\n\nexport function getDotMatrixBackgroundGeometry(\n  seed: number,\n): DotMatrixBackgroundPulse[] {\n  const random = mulberry32(seed);\n\n  return Array.from({ length: dotMatrixBackgroundPulseCount }, () => ({\n    originX: Math.round(seededRange(random, 15, 85)),\n    originY: Math.round(seededRange(random, 15, 85)),\n    delay: roundTo(seededRange(random, 0, 3), 2),\n    repeatDelay: roundTo(seededRange(random, 0.5, 2.5), 2),\n  }));\n}\n\nconst dotMatrixBackgroundRootClasses = \"relative isolate overflow-hidden\";\n\nconst dotMatrixBackgroundLayerClasses =\n  \"pointer-events-none absolute inset-0 select-none\";\n\nconst dotMatrixBackgroundContentClasses = \"relative z-10\";\n\nconst dotMatrixBackgroundPatternClasses =\n  \"absolute inset-0 [background-image:radial-gradient(circle,currentColor_1.5px,transparent_1.5px)]\";\n\nconst dotMatrixBackgroundDensityClasses: Record<\n  DotMatrixBackgroundDensity,\n  string\n> = {\n  sparse: \"[background-size:48px_48px]\",\n  normal: \"[background-size:32px_32px]\",\n  dense: \"[background-size:20px_20px]\",\n};\n\nconst dotMatrixBackgroundToneIntensityClasses: Record<\n  DotMatrixBackgroundTone,\n  Record<DotMatrixBackgroundIntensity, string>\n> = {\n  foreground: {\n    faint: \"text-foreground/10 dark:text-foreground/15\",\n    subtle: \"text-foreground/15 dark:text-foreground/20\",\n    bold: \"text-foreground/20 dark:text-foreground/30\",\n  },\n  muted: {\n    faint: \"text-muted-foreground/15 dark:text-muted-foreground/20\",\n    subtle: \"text-muted-foreground/20 dark:text-muted-foreground/30\",\n    bold: \"text-muted-foreground/30 dark:text-muted-foreground/45\",\n  },\n  primary: {\n    faint: \"text-primary/10 dark:text-primary/15\",\n    subtle: \"text-primary/15 dark:text-primary/25\",\n    bold: \"text-primary/25 dark:text-primary/35\",\n  },\n};\n\nconst dotMatrixBackgroundPulseToneClasses: Record<\n  DotMatrixBackgroundTone,\n  string\n> = {\n  foreground: \"text-foreground/50 dark:text-foreground/60\",\n  muted: \"text-muted-foreground/50 dark:text-muted-foreground/60\",\n  primary: \"text-primary/70 dark:text-primary/80\",\n};\n\nexport interface DotMatrixBackgroundClassNamesOptions {\n  className?: string;\n}\n\nexport function dotMatrixBackgroundClassNames({\n  className,\n}: DotMatrixBackgroundClassNamesOptions = {}) {\n  return cn(dotMatrixBackgroundRootClasses, className);\n}\n\nexport interface DotMatrixBackgroundLayerClassNamesOptions {\n  className?: string;\n  intensity?: DotMatrixBackgroundIntensity;\n  tone?: DotMatrixBackgroundTone;\n}\n\nexport function dotMatrixBackgroundLayerClassNames({\n  className,\n  intensity = \"subtle\",\n  tone = \"muted\",\n}: DotMatrixBackgroundLayerClassNamesOptions = {}) {\n  return cn(\n    dotMatrixBackgroundLayerClasses,\n    dotMatrixBackgroundToneIntensityClasses[tone][intensity],\n    className,\n  );\n}\n\nexport interface DotMatrixBackgroundContentClassNamesOptions {\n  className?: string;\n}\n\nexport function dotMatrixBackgroundContentClassNames({\n  className,\n}: DotMatrixBackgroundContentClassNamesOptions = {}) {\n  return cn(dotMatrixBackgroundContentClasses, className);\n}\n\n// The pulse is a full-bleed copy of the dot pattern so its dots align exactly\n// with the base grid; a fixed radial mask at the seeded origin limits it to a\n// local glow. Brightness animates via opacity only — scaling would misalign\n// the pattern. Browsers without mask support degrade to a whole-field pulse.\nfunction pulseMaskStyle(pulse: DotMatrixBackgroundPulse): CSSProperties {\n  const mask = `radial-gradient(circle at ${pulse.originX}% ${pulse.originY}%, black 0%, black 12%, transparent 32%)`;\n\n  return {\n    maskImage: mask,\n    WebkitMaskImage: mask,\n  };\n}\n\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nfunction useHasHydrated() {\n  const [hasHydrated, setHasHydrated] = useState(false);\n\n  useIsomorphicLayoutEffect(() => {\n    setHasHydrated(true);\n  }, []);\n\n  return hasHydrated;\n}\n\n// Pauses looping pulses while the background is scrolled offscreen. Fails open\n// (stays true) where IntersectionObserver is unavailable so the background\n// still animates in older browsers and jsdom.\nfunction useDecorativeInView(ref: { current: HTMLElement | null }) {\n  const [inView, setInView] = useState(true);\n\n  useEffect(() => {\n    const element = ref.current;\n\n    if (\n      !element ||\n      typeof window === \"undefined\" ||\n      !(\"IntersectionObserver\" in window)\n    ) {\n      return;\n    }\n\n    const observer = new window.IntersectionObserver(([entry]) => {\n      if (entry) {\n        setInView(entry.isIntersecting);\n      }\n    });\n\n    observer.observe(element);\n\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return inView;\n}\n\nexport const DotMatrixBackground = forwardRef(function DotMatrixBackground(\n  {\n    animate = true,\n    children,\n    className,\n    density = \"normal\",\n    intensity = \"subtle\",\n    interactive = true,\n    mode = \"pulse\",\n    onPointerLeave: onPointerLeaveProp,\n    onPointerMove: onPointerMoveProp,\n    seed = 1,\n    speed = \"normal\",\n    tone = \"muted\",\n    ...props\n  }: DotMatrixBackgroundProps,\n  forwardedRef: ForwardedRef<HTMLDivElement>,\n) {\n  const localRef = useRef<HTMLDivElement | null>(null);\n  const composedRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      localRef.current = node;\n\n      if (typeof forwardedRef === \"function\") {\n        forwardedRef(node);\n      } else if (forwardedRef) {\n        forwardedRef.current = node;\n      }\n    },\n    [forwardedRef],\n  );\n\n  const hasHydrated = useHasHydrated();\n  const prefersReducedMotion = useReducedMotion();\n  const inView = useDecorativeInView(localRef);\n  // OS preference is only trusted after hydration so the first client render\n  // matches the server render exactly; animate={false} is deterministic on\n  // both sides and applies immediately.\n  const reducedMotion =\n    !animate || (hasHydrated && prefersReducedMotion === true);\n  const pageVisible = usePageVisible();\n  const running = hasHydrated && !reducedMotion && inView && pageVisible;\n\n  const motionConfig = getDotMatrixBackgroundMotionConfig(speed, reducedMotion);\n  const pulses = useMemo(() => getDotMatrixBackgroundGeometry(seed), [seed]);\n\n  // The follow highlight is driven by Motion values rather than React state,\n  // keeping pointer movement off the render path. A spring makes the local\n  // dot glow trail the mouse without changing the underlying grid alignment.\n  const pointerX = useMotionValue(50);\n  const pointerY = useMotionValue(50);\n  const followX = useSpring(pointerX, { stiffness: 180, damping: 26 });\n  const followY = useSpring(pointerY, { stiffness: 180, damping: 26 });\n  const followMask = useMotionTemplate`radial-gradient(circle at ${followX}% ${followY}%, black 0%, black 12%, transparent 32%)`;\n  const [pointerActive, setPointerActive] = useState(false);\n  const followEnabled = running && interactive && mode === \"follow\";\n\n  if (!followEnabled && pointerActive) {\n    setPointerActive(false);\n  }\n\n  const handlePointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      onPointerMoveProp?.(event);\n\n      // This is a non-essential mouse enhancement. Touch remains free to\n      // scroll and operate the content without producing decorative motion.\n      if (!followEnabled || event.pointerType === \"touch\") {\n        return;\n      }\n\n      const node = localRef.current;\n\n      if (!node) {\n        return;\n      }\n\n      const rect = node.getBoundingClientRect();\n\n      if (rect.width === 0 || rect.height === 0) {\n        return;\n      }\n\n      pointerX.set(\n        Math.min(\n          100,\n          Math.max(0, ((event.clientX - rect.left) / rect.width) * 100),\n        ),\n      );\n      pointerY.set(\n        Math.min(\n          100,\n          Math.max(0, ((event.clientY - rect.top) / rect.height) * 100),\n        ),\n      );\n      setPointerActive(true);\n    },\n    [followEnabled, onPointerMoveProp, pointerX, pointerY],\n  );\n\n  const handlePointerLeave = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      onPointerLeaveProp?.(event);\n      setPointerActive(false);\n    },\n    [onPointerLeaveProp],\n  );\n\n  const patternClassName = cn(\n    dotMatrixBackgroundPatternClasses,\n    dotMatrixBackgroundDensityClasses[density],\n  );\n  const pulseClassName = cn(\n    patternClassName,\n    dotMatrixBackgroundPulseToneClasses[tone],\n  );\n\n  return (\n    <MotionConfig reducedMotion={reducedMotion ? \"always\" : \"user\"}>\n      <div\n        {...props}\n        ref={composedRef}\n        data-slot=\"dot-matrix-background\"\n        data-animate={animate ? \"true\" : \"false\"}\n        data-density={density}\n        data-intensity={intensity}\n        data-interactive={interactive ? \"true\" : \"false\"}\n        data-mode={mode}\n        data-speed={speed}\n        data-tone={tone}\n        data-reduced-motion={reducedMotion ? \"true\" : undefined}\n        className={dotMatrixBackgroundClassNames({ className })}\n        onPointerMove={handlePointerMove}\n        onPointerLeave={handlePointerLeave}\n      >\n        <div\n          aria-hidden=\"true\"\n          data-slot=\"dot-matrix-background-layer\"\n          className={dotMatrixBackgroundLayerClassNames({ intensity, tone })}\n        >\n          <div className={patternClassName} />\n          {followEnabled ? (\n            <motionElement.div\n              data-slot=\"dot-matrix-background-follow\"\n              className={pulseClassName}\n              style={{ maskImage: followMask, WebkitMaskImage: followMask }}\n              initial={false}\n              animate={{ opacity: pointerActive ? 1 : 0 }}\n              transition={{ duration: 0.2, ease: \"easeOut\" }}\n            />\n          ) : running ? (\n            pulses.map((pulse, index) => (\n              <motionElement.div\n                key={index}\n                data-slot=\"dot-matrix-background-pulse\"\n                className={pulseClassName}\n                style={pulseMaskStyle(pulse)}\n                initial={{ opacity: 0 }}\n                animate={{ opacity: [0, 1, 0] }}\n                transition={{\n                  ...motionConfig.transition,\n                  delay: pulse.delay,\n                  repeatDelay: pulse.repeatDelay,\n                }}\n              />\n            ))\n          ) : (\n            pulses\n              .slice(0, 1)\n              .map((pulse, index) => (\n                <div\n                  key={index}\n                  data-slot=\"dot-matrix-background-pulse\"\n                  className={cn(pulseClassName, \"opacity-50\")}\n                  style={pulseMaskStyle(pulse)}\n                />\n              ))\n          )}\n        </div>\n        <div\n          data-slot=\"dot-matrix-background-content\"\n          className={dotMatrixBackgroundContentClassNames()}\n        >\n          {children}\n        </div>\n      </div>\n    </MotionConfig>\n  );\n});\n\nDotMatrixBackground.displayName = \"DotMatrixBackground\";\n"
    },
    {
      "path": "packages/components/src/components/dot-matrix-background/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/dot-matrix-background/index.ts",
      "content": "export {\n  DotMatrixBackground,\n  dotMatrixBackgroundClassNames,\n  dotMatrixBackgroundContentClassNames,\n  dotMatrixBackgroundLayerClassNames,\n  getDotMatrixBackgroundGeometry,\n  getDotMatrixBackgroundMotionConfig,\n  type DotMatrixBackgroundClassNamesOptions,\n  type DotMatrixBackgroundContentClassNamesOptions,\n  type DotMatrixBackgroundDensity,\n  type DotMatrixBackgroundIntensity,\n  type DotMatrixBackgroundLayerClassNamesOptions,\n  type DotMatrixBackgroundMode,\n  type DotMatrixBackgroundMotionConfig,\n  type DotMatrixBackgroundProps,\n  type DotMatrixBackgroundPulse,\n  type DotMatrixBackgroundSpeed,\n  type DotMatrixBackgroundTone,\n} from \"./dot-matrix-background\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "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": "packages/components/src/utils/seeded-random.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/seeded-random.ts",
      "content": "/**\n * Deterministic pseudo-random helpers for decorative components. Background\n * components must render identical markup on the server and every client\n * render, so all \"random\" placement flows through a caller-provided seed\n * instead of Math.random().\n */\nexport function mulberry32(seed: number): () => number {\n  let state = seed >>> 0;\n\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0;\n    let t = state;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport function seededRange(\n  random: () => number,\n  min: number,\n  max: number,\n): number {\n  return min + (max - min) * random();\n}\n\nexport function seededPick<T>(random: () => number, values: readonly T[]): T {\n  return values[\n    Math.min(values.length - 1, Math.floor(random() * values.length))\n  ];\n}\n"
    }
  ]
}
