{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "starfield-background",
  "type": "registry:ui",
  "title": "StarfieldBackground",
  "description": "Animated landing-page background: a three-layer parallax starfield of seeded token-colored SVG stars with slow drift, twinkle, and optional spring-smoothed pointer parallax. 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/starfield-background/starfield-background.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/starfield-background/starfield-background.tsx",
      "content": "import {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\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  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\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 StarfieldBackgroundDensity = \"sparse\" | \"normal\" | \"dense\";\nexport type StarfieldBackgroundIntensity = \"faint\" | \"subtle\" | \"bold\";\nexport type StarfieldBackgroundSpeed = \"slow\" | \"normal\" | \"fast\";\nexport type StarfieldBackgroundTone = \"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 StarfieldBackgroundProps extends MotionSafeDivProps {\n  animate?: boolean;\n  children?: ReactNode;\n  density?: StarfieldBackgroundDensity;\n  intensity?: StarfieldBackgroundIntensity;\n  interactive?: boolean;\n  seed?: number;\n  speed?: StarfieldBackgroundSpeed;\n  tone?: StarfieldBackgroundTone;\n}\n\nexport interface StarfieldBackgroundMotionConfig {\n  animateEnabled: boolean;\n  driftDurations: readonly [number, number, number];\n  parallaxDepths: readonly [number, number, number];\n  reducedMotion: boolean;\n  twinkleTransition: Transition;\n}\n\nexport interface StarfieldBackgroundStar {\n  cx: number;\n  cy: number;\n}\n\nexport interface StarfieldBackgroundTwinkle {\n  cx: number;\n  cy: number;\n  delay: number;\n  duration: number;\n}\n\nexport interface StarfieldBackgroundLayer {\n  radius: number;\n  stars: StarfieldBackgroundStar[];\n  twinkles: StarfieldBackgroundTwinkle[];\n}\n\nconst starfieldBackgroundBaseDriftDurations = [120, 90, 60] as const;\n\nconst starfieldBackgroundParallaxDepths = [3, 6, 10] as const;\n\nconst starfieldBackgroundSpeedMultipliers: Record<\n  StarfieldBackgroundSpeed,\n  number\n> = {\n  slow: 1.5,\n  normal: 1,\n  fast: 0.6,\n};\n\nexport function getStarfieldBackgroundMotionConfig(\n  speed: StarfieldBackgroundSpeed,\n  reducedMotion = false,\n): StarfieldBackgroundMotionConfig {\n  if (reducedMotion) {\n    return {\n      animateEnabled: false,\n      driftDurations: [0, 0, 0],\n      parallaxDepths: [0, 0, 0],\n      reducedMotion: true,\n      twinkleTransition: { duration: 0 },\n    };\n  }\n\n  const multiplier = starfieldBackgroundSpeedMultipliers[speed];\n\n  return {\n    animateEnabled: true,\n    driftDurations: [\n      starfieldBackgroundBaseDriftDurations[0] * multiplier,\n      starfieldBackgroundBaseDriftDurations[1] * multiplier,\n      starfieldBackgroundBaseDriftDurations[2] * multiplier,\n    ],\n    parallaxDepths: starfieldBackgroundParallaxDepths,\n    reducedMotion: false,\n    twinkleTransition: { ease: \"easeInOut\", repeat: Infinity },\n  };\n}\n\nconst starfieldBackgroundStarCounts: Record<\n  StarfieldBackgroundDensity,\n  readonly [number, number, number]\n> = {\n  sparse: [24, 16, 10],\n  normal: [40, 26, 16],\n  dense: [60, 40, 24],\n};\n\nconst starfieldBackgroundTwinkleCounts: Record<\n  StarfieldBackgroundDensity,\n  number\n> = {\n  sparse: 4,\n  normal: 6,\n  dense: 8,\n};\n\nconst starfieldBackgroundLayerRadii = [0.4, 0.6, 0.9] as const;\n\nfunction roundTo(value: number, decimals: number): number {\n  const factor = 10 ** decimals;\n  return Math.round(value * factor) / factor;\n}\n\nexport function getStarfieldBackgroundGeometry(\n  seed: number,\n  density: StarfieldBackgroundDensity,\n): StarfieldBackgroundLayer[] {\n  const random = mulberry32(seed);\n  const twinkleCount = starfieldBackgroundTwinkleCounts[density];\n\n  return starfieldBackgroundStarCounts[density].map(\n    (starCount, layerIndex) => ({\n      radius: starfieldBackgroundLayerRadii[layerIndex],\n      stars: Array.from({ length: starCount }, () => ({\n        cx: roundTo(seededRange(random, 2, 98), 1),\n        cy: roundTo(seededRange(random, 2, 98), 1),\n      })),\n      twinkles: Array.from({ length: twinkleCount }, () => ({\n        cx: roundTo(seededRange(random, 2, 98), 1),\n        cy: roundTo(seededRange(random, 2, 98), 1),\n        delay: roundTo(seededRange(random, 0, 4), 2),\n        duration: roundTo(seededRange(random, 2, 5), 2),\n      })),\n    }),\n  );\n}\n\nconst starfieldBackgroundRootClasses = \"relative isolate overflow-hidden\";\n\nconst starfieldBackgroundLayerClasses =\n  \"pointer-events-none absolute inset-0 select-none\";\n\nconst starfieldBackgroundContentClasses = \"relative z-10\";\n\n// Oversized so the slow drift loop never exposes an empty edge.\nconst starfieldBackgroundStarLayerClasses = \"absolute -inset-[4%]\";\n\nconst starfieldBackgroundSvgClasses = \"absolute inset-0 h-full w-full\";\n\nconst starfieldBackgroundLayerOpacityClasses = [\n  \"opacity-60\",\n  \"opacity-75\",\n  \"opacity-90\",\n] as const;\n\nconst starfieldBackgroundToneIntensityClasses: Record<\n  StarfieldBackgroundTone,\n  Record<StarfieldBackgroundIntensity, string>\n> = {\n  foreground: {\n    faint: \"text-foreground/30 dark:text-foreground/40\",\n    subtle: \"text-foreground/50 dark:text-foreground/60\",\n    bold: \"text-foreground/70 dark:text-foreground/80\",\n  },\n  muted: {\n    faint: \"text-muted-foreground/30 dark:text-muted-foreground/40\",\n    subtle: \"text-muted-foreground/50 dark:text-muted-foreground/60\",\n    bold: \"text-muted-foreground/70 dark:text-muted-foreground/80\",\n  },\n  primary: {\n    faint: \"text-primary/30 dark:text-primary/40\",\n    subtle: \"text-primary/50 dark:text-primary/60\",\n    bold: \"text-primary/70 dark:text-primary/80\",\n  },\n};\n\n// Literal per-layer drift keyframes so layers never move in lockstep.\nconst starfieldBackgroundDriftKeyframes = [\n  { x: [\"0%\", \"1.6%\"], y: [\"0%\", \"-1.2%\"] },\n  { x: [\"0%\", \"-1.4%\"], y: [\"0%\", \"1%\"] },\n  { x: [\"0%\", \"1.2%\"], y: [\"0%\", \"0.8%\"] },\n] as const;\n\nexport interface StarfieldBackgroundClassNamesOptions {\n  className?: string;\n}\n\nexport function starfieldBackgroundClassNames({\n  className,\n}: StarfieldBackgroundClassNamesOptions = {}) {\n  return cn(starfieldBackgroundRootClasses, className);\n}\n\nexport interface StarfieldBackgroundLayerClassNamesOptions {\n  className?: string;\n  intensity?: StarfieldBackgroundIntensity;\n  tone?: StarfieldBackgroundTone;\n}\n\nexport function starfieldBackgroundLayerClassNames({\n  className,\n  intensity = \"subtle\",\n  tone = \"muted\",\n}: StarfieldBackgroundLayerClassNamesOptions = {}) {\n  return cn(\n    starfieldBackgroundLayerClasses,\n    starfieldBackgroundToneIntensityClasses[tone][intensity],\n    className,\n  );\n}\n\nexport interface StarfieldBackgroundContentClassNamesOptions {\n  className?: string;\n}\n\nexport function starfieldBackgroundContentClassNames({\n  className,\n}: StarfieldBackgroundContentClassNamesOptions = {}) {\n  return cn(starfieldBackgroundContentClasses, className);\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 drift and twinkle loops while the background is scrolled offscreen.\n// Fails open (stays true) where IntersectionObserver is unavailable so the\n// background 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\nfunction StarfieldStars({\n  layer,\n  layerIndex,\n  running,\n  twinkleTransition,\n}: {\n  layer: StarfieldBackgroundLayer;\n  layerIndex: number;\n  running: boolean;\n  twinkleTransition: Transition;\n}) {\n  return (\n    <svg\n      className={cn(\n        starfieldBackgroundSvgClasses,\n        starfieldBackgroundLayerOpacityClasses[layerIndex],\n      )}\n      viewBox=\"0 0 100 100\"\n      preserveAspectRatio=\"xMidYMid slice\"\n      focusable=\"false\"\n    >\n      {layer.stars.map((star, starIndex) => (\n        <circle\n          key={`star-${starIndex}`}\n          cx={star.cx}\n          cy={star.cy}\n          r={layer.radius}\n          fill=\"currentColor\"\n        />\n      ))}\n      {layer.twinkles.map((twinkle, twinkleIndex) =>\n        running ? (\n          <motionElement.circle\n            key={`twinkle-${twinkleIndex}`}\n            data-slot=\"starfield-background-twinkle\"\n            cx={twinkle.cx}\n            cy={twinkle.cy}\n            r={layer.radius * 1.4}\n            fill=\"currentColor\"\n            initial={{ opacity: 0.2 }}\n            animate={{ opacity: [0.2, 1, 0.2] }}\n            transition={{\n              ...twinkleTransition,\n              delay: twinkle.delay,\n              duration: twinkle.duration,\n            }}\n          />\n        ) : (\n          <circle\n            key={`twinkle-${twinkleIndex}`}\n            data-slot=\"starfield-background-twinkle\"\n            cx={twinkle.cx}\n            cy={twinkle.cy}\n            r={layer.radius * 1.4}\n            fill=\"currentColor\"\n            opacity={0.6}\n          />\n        ),\n      )}\n    </svg>\n  );\n}\n\nexport const StarfieldBackground = forwardRef(function StarfieldBackground(\n  {\n    animate = true,\n    children,\n    className,\n    density = \"normal\",\n    intensity = \"subtle\",\n    interactive = true,\n    onPointerLeave: onPointerLeaveProp,\n    onPointerMove: onPointerMoveProp,\n    seed = 1,\n    speed = \"normal\",\n    tone = \"muted\",\n    ...props\n  }: StarfieldBackgroundProps,\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 = getStarfieldBackgroundMotionConfig(speed, reducedMotion);\n  const layers = useMemo(\n    () => getStarfieldBackgroundGeometry(seed, density),\n    [seed, density],\n  );\n\n  // Pointer parallax runs entirely through motion values — no React state per\n  // pointer event. Springs ease each layer toward the cursor with depth\n  // increasing from the far to the near layer.\n  const pointerX = useMotionValue(0);\n  const pointerY = useMotionValue(0);\n  const springX = useSpring(pointerX, { stiffness: 60, damping: 20 });\n  const springY = useSpring(pointerY, { stiffness: 60, damping: 20 });\n  const farParallaxX = useTransform(springX, [-0.5, 0.5], [-3, 3]);\n  const farParallaxY = useTransform(springY, [-0.5, 0.5], [-3, 3]);\n  const midParallaxX = useTransform(springX, [-0.5, 0.5], [-6, 6]);\n  const midParallaxY = useTransform(springY, [-0.5, 0.5], [-6, 6]);\n  const nearParallaxX = useTransform(springX, [-0.5, 0.5], [-10, 10]);\n  const nearParallaxY = useTransform(springY, [-0.5, 0.5], [-10, 10]);\n  const parallax = [\n    { x: farParallaxX, y: farParallaxY },\n    { x: midParallaxX, y: midParallaxY },\n    { x: nearParallaxX, y: nearParallaxY },\n  ];\n\n  const parallaxEnabled = running && interactive;\n\n  const handlePointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      onPointerMoveProp?.(event);\n\n      if (!parallaxEnabled) {\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((event.clientX - rect.left) / rect.width - 0.5);\n      pointerY.set((event.clientY - rect.top) / rect.height - 0.5);\n    },\n    [onPointerMoveProp, parallaxEnabled, pointerX, pointerY],\n  );\n\n  const handlePointerLeave = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      onPointerLeaveProp?.(event);\n      pointerX.set(0);\n      pointerY.set(0);\n    },\n    [onPointerLeaveProp, pointerX, pointerY],\n  );\n\n  return (\n    <MotionConfig reducedMotion={reducedMotion ? \"always\" : \"user\"}>\n      <div\n        {...props}\n        ref={composedRef}\n        data-slot=\"starfield-background\"\n        data-animate={animate ? \"true\" : \"false\"}\n        data-density={density}\n        data-intensity={intensity}\n        data-interactive={interactive ? \"true\" : \"false\"}\n        data-speed={speed}\n        data-tone={tone}\n        data-reduced-motion={reducedMotion ? \"true\" : undefined}\n        className={starfieldBackgroundClassNames({ className })}\n        onPointerMove={handlePointerMove}\n        onPointerLeave={handlePointerLeave}\n      >\n        <div\n          aria-hidden=\"true\"\n          data-slot=\"starfield-background-layer\"\n          className={starfieldBackgroundLayerClassNames({ intensity, tone })}\n        >\n          {layers.map((layer, layerIndex) =>\n            running ? (\n              <motionElement.div\n                key={layerIndex}\n                data-slot=\"starfield-background-star-layer\"\n                className={starfieldBackgroundStarLayerClasses}\n                style={{\n                  x: parallax[layerIndex].x,\n                  y: parallax[layerIndex].y,\n                }}\n              >\n                <motionElement.div\n                  className=\"absolute inset-0\"\n                  animate={{\n                    x: [...starfieldBackgroundDriftKeyframes[layerIndex].x],\n                    y: [...starfieldBackgroundDriftKeyframes[layerIndex].y],\n                  }}\n                  transition={{\n                    duration: motionConfig.driftDurations[layerIndex],\n                    ease: \"easeInOut\",\n                    repeat: Infinity,\n                    repeatType: \"mirror\",\n                  }}\n                >\n                  <StarfieldStars\n                    layer={layer}\n                    layerIndex={layerIndex}\n                    running\n                    twinkleTransition={motionConfig.twinkleTransition}\n                  />\n                </motionElement.div>\n              </motionElement.div>\n            ) : (\n              <div\n                key={layerIndex}\n                data-slot=\"starfield-background-star-layer\"\n                className={starfieldBackgroundStarLayerClasses}\n              >\n                <StarfieldStars\n                  layer={layer}\n                  layerIndex={layerIndex}\n                  running={false}\n                  twinkleTransition={motionConfig.twinkleTransition}\n                />\n              </div>\n            ),\n          )}\n        </div>\n        <div\n          data-slot=\"starfield-background-content\"\n          className={starfieldBackgroundContentClassNames()}\n        >\n          {children}\n        </div>\n      </div>\n    </MotionConfig>\n  );\n});\n\nStarfieldBackground.displayName = \"StarfieldBackground\";\n"
    },
    {
      "path": "packages/components/src/components/starfield-background/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/starfield-background/index.ts",
      "content": "export {\n  StarfieldBackground,\n  getStarfieldBackgroundGeometry,\n  getStarfieldBackgroundMotionConfig,\n  starfieldBackgroundClassNames,\n  starfieldBackgroundContentClassNames,\n  starfieldBackgroundLayerClassNames,\n  type StarfieldBackgroundClassNamesOptions,\n  type StarfieldBackgroundContentClassNamesOptions,\n  type StarfieldBackgroundDensity,\n  type StarfieldBackgroundIntensity,\n  type StarfieldBackgroundLayer,\n  type StarfieldBackgroundLayerClassNamesOptions,\n  type StarfieldBackgroundMotionConfig,\n  type StarfieldBackgroundProps,\n  type StarfieldBackgroundSpeed,\n  type StarfieldBackgroundStar,\n  type StarfieldBackgroundTone,\n  type StarfieldBackgroundTwinkle,\n} from \"./starfield-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"
    }
  ]
}
