{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-background",
  "type": "registry:ui",
  "title": "AuroraBackground",
  "description": "Animated landing-page background: flowing aurora ribbons of heavily blurred, hue-shifted gradient light that drift and breathe behind hero content. Colors derive from one semantic tone token via CSS relative color syntax with a mono-hue fallback. 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/aurora-background/aurora-background.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/aurora-background/aurora-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 ReactNode,\n} from \"react\";\nimport {\n  MotionConfig,\n  motion as motionElement,\n  useReducedMotion,\n  type Transition,\n} from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { usePageVisible } from \"../../utils/use-page-visible\";\nimport { mulberry32, seededPick, seededRange } from \"../../utils/seeded-random\";\n\nexport type AuroraBackgroundDensity = \"sparse\" | \"normal\" | \"dense\";\nexport type AuroraBackgroundIntensity = \"faint\" | \"subtle\" | \"bold\";\nexport type AuroraBackgroundSpeed = \"slow\" | \"normal\" | \"fast\";\nexport type AuroraBackgroundTone = \"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 AuroraBackgroundProps extends MotionSafeDivProps {\n  animate?: boolean;\n  children?: ReactNode;\n  density?: AuroraBackgroundDensity;\n  intensity?: AuroraBackgroundIntensity;\n  seed?: number;\n  speed?: AuroraBackgroundSpeed;\n  tone?: AuroraBackgroundTone;\n}\n\nexport interface AuroraBackgroundMotionConfig {\n  animateEnabled: boolean;\n  driftDuration: number;\n  reducedMotion: boolean;\n  transition: Transition;\n}\n\nexport interface AuroraBackgroundRibbon {\n  delay: number;\n  driftX: number;\n  driftY: number;\n  durationScale: number;\n  rotationClassName: string;\n  swayDegrees: number;\n  top: number;\n}\n\nconst auroraBackgroundSpeedDurations: Record<AuroraBackgroundSpeed, number> = {\n  slow: 32,\n  normal: 22,\n  fast: 14,\n};\n\nexport function getAuroraBackgroundMotionConfig(\n  speed: AuroraBackgroundSpeed,\n  reducedMotion = false,\n): AuroraBackgroundMotionConfig {\n  if (reducedMotion) {\n    return {\n      animateEnabled: false,\n      driftDuration: 0,\n      reducedMotion: true,\n      transition: { duration: 0 },\n    };\n  }\n\n  const driftDuration = auroraBackgroundSpeedDurations[speed];\n\n  return {\n    animateEnabled: true,\n    driftDuration,\n    reducedMotion: false,\n    transition: {\n      duration: driftDuration,\n      ease: \"easeInOut\",\n      repeat: Infinity,\n      repeatType: \"mirror\",\n    },\n  };\n}\n\nconst auroraBackgroundRibbonCounts: Record<AuroraBackgroundDensity, number> = {\n  sparse: 3,\n  normal: 4,\n  dense: 5,\n};\n\n// Base rotations come from a literal class set (never inline transform styles)\n// so the server markup stays free of transforms and Tailwind can see every\n// class statically.\nconst auroraBackgroundRotationClasses = [\n  \"-rotate-12\",\n  \"-rotate-6\",\n  \"-rotate-3\",\n  \"rotate-3\",\n  \"rotate-6\",\n  \"rotate-12\",\n] 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 getAuroraBackgroundGeometry(\n  seed: number,\n  density: AuroraBackgroundDensity,\n): AuroraBackgroundRibbon[] {\n  const random = mulberry32(seed);\n\n  return Array.from({ length: auroraBackgroundRibbonCounts[density] }, () => ({\n    top: Math.round(seededRange(random, -15, 65)),\n    rotationClassName: seededPick(random, auroraBackgroundRotationClasses),\n    driftX: Math.round(seededRange(random, 8, 18)),\n    driftY: Math.round(seededRange(random, 4, 10)),\n    swayDegrees: roundTo(seededRange(random, 2, 6), 2),\n    delay: roundTo(seededRange(random, 0, 6), 2),\n    durationScale: roundTo(seededRange(random, 0.85, 1.25), 2),\n  }));\n}\n\n// Each ribbon slot shifts the hue of the active tone color by a fixed number\n// of OKLCH degrees, so one semantic token yields a designed multi-hue aurora.\n// The spread is wide enough that neighboring slots land on clearly distinct\n// color families rather than tints of the same hue.\nexport const auroraBackgroundHueShifts = [0, 75, -60, 145, -115] as const;\n\nconst auroraBackgroundRootClasses = \"relative isolate overflow-hidden\";\n\nconst auroraBackgroundLayerClasses =\n  \"pointer-events-none absolute inset-0 select-none\";\n\nconst auroraBackgroundContentClasses = \"relative z-10\";\n\nconst auroraBackgroundRailClasses = \"absolute -inset-x-1/3 h-64\";\n\n// The hue rotation is a progressive enhancement: browsers without relative\n// color syntax keep plain currentColor, which reads as a designed mono-hue\n// aurora. The chroma floor and lightness clamp keep the rotation visible and\n// vibrant when the tone token is a near-neutral (the default theme's\n// primary/foreground tokens have zero chroma, where a bare hue rotation is a\n// no-op); tokens that already carry chroma and mid lightness pass through\n// unchanged. The alpha multiplier compensates for blur spreading the color\n// thin while preserving the tone/intensity alpha ladder proportionally. Blur\n// is a static style and must never be animated.\nconst auroraBackgroundRibbonClasses =\n  \"h-full w-full rounded-[100%] bg-gradient-to-r from-transparent via-current to-transparent blur-2xl supports-[color:oklch(from_red_l_c_h)]:[color:oklch(from_currentcolor_clamp(0.65,l,0.75)_max(c,0.19)_calc(h_+_var(--aurora-hue-shift,0))/calc(alpha*1.35))]\";\n\nconst auroraBackgroundStaticRibbonOpacityClasses = [\n  \"opacity-70\",\n  \"opacity-50\",\n  \"opacity-60\",\n  \"opacity-40\",\n  \"opacity-55\",\n] as const;\n\n// Ribbons cover far more surface than the family's thin geometry, so each\n// tier sits one step below the geometric backgrounds' alpha values to keep\n// hero copy readable, especially in dark mode.\nconst auroraBackgroundToneIntensityClasses: Record<\n  AuroraBackgroundTone,\n  Record<AuroraBackgroundIntensity, string>\n> = {\n  foreground: {\n    faint: \"text-foreground/15 dark:text-foreground/15\",\n    subtle: \"text-foreground/25 dark:text-foreground/25\",\n    bold: \"text-foreground/35 dark:text-foreground/40\",\n  },\n  muted: {\n    faint: \"text-muted-foreground/20 dark:text-muted-foreground/20\",\n    subtle: \"text-muted-foreground/30 dark:text-muted-foreground/30\",\n    bold: \"text-muted-foreground/45 dark:text-muted-foreground/45\",\n  },\n  primary: {\n    faint: \"text-primary/20 dark:text-primary/20\",\n    subtle: \"text-primary/35 dark:text-primary/35\",\n    bold: \"text-primary/50 dark:text-primary/50\",\n  },\n};\n\nexport interface AuroraBackgroundClassNamesOptions {\n  className?: string;\n}\n\nexport function auroraBackgroundClassNames({\n  className,\n}: AuroraBackgroundClassNamesOptions = {}) {\n  return cn(auroraBackgroundRootClasses, className);\n}\n\nexport interface AuroraBackgroundLayerClassNamesOptions {\n  className?: string;\n  intensity?: AuroraBackgroundIntensity;\n  tone?: AuroraBackgroundTone;\n}\n\nexport function auroraBackgroundLayerClassNames({\n  className,\n  intensity = \"subtle\",\n  tone = \"primary\",\n}: AuroraBackgroundLayerClassNamesOptions = {}) {\n  return cn(\n    auroraBackgroundLayerClasses,\n    auroraBackgroundToneIntensityClasses[tone][intensity],\n    className,\n  );\n}\n\nexport interface AuroraBackgroundContentClassNamesOptions {\n  className?: string;\n}\n\nexport function auroraBackgroundContentClassNames({\n  className,\n}: AuroraBackgroundContentClassNamesOptions = {}) {\n  return cn(auroraBackgroundContentClasses, 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 looping drifts 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\nfunction railStyle(ribbon: AuroraBackgroundRibbon): CSSProperties {\n  return { top: `${ribbon.top}%` };\n}\n\nfunction ribbonStyle(slot: number): CSSProperties {\n  return {\n    \"--aurora-hue-shift\": `${auroraBackgroundHueShifts[slot % auroraBackgroundHueShifts.length]}`,\n  } as CSSProperties;\n}\n\nexport const AuroraBackground = forwardRef(function AuroraBackground(\n  {\n    animate = true,\n    children,\n    className,\n    density = \"normal\",\n    intensity = \"subtle\",\n    seed = 1,\n    speed = \"normal\",\n    tone = \"primary\",\n    ...props\n  }: AuroraBackgroundProps,\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 = getAuroraBackgroundMotionConfig(speed, reducedMotion);\n  const ribbons = useMemo(\n    () => getAuroraBackgroundGeometry(seed, density),\n    [seed, density],\n  );\n\n  return (\n    <MotionConfig reducedMotion={reducedMotion ? \"always\" : \"user\"}>\n      <div\n        {...props}\n        ref={composedRef}\n        data-slot=\"aurora-background\"\n        data-animate={animate ? \"true\" : \"false\"}\n        data-density={density}\n        data-intensity={intensity}\n        data-speed={speed}\n        data-tone={tone}\n        data-reduced-motion={reducedMotion ? \"true\" : undefined}\n        className={auroraBackgroundClassNames({ className })}\n      >\n        <div\n          aria-hidden=\"true\"\n          data-slot=\"aurora-background-layer\"\n          className={auroraBackgroundLayerClassNames({ intensity, tone })}\n        >\n          {ribbons.map((ribbon, slot) => (\n            <div\n              key={slot}\n              className={cn(\n                auroraBackgroundRailClasses,\n                ribbon.rotationClassName,\n              )}\n              style={railStyle(ribbon)}\n            >\n              {running ? (\n                <motionElement.div\n                  data-slot=\"aurora-background-ribbon\"\n                  className={auroraBackgroundRibbonClasses}\n                  style={ribbonStyle(slot)}\n                  initial={{\n                    opacity: 1,\n                    rotate: -ribbon.swayDegrees,\n                    scaleY: 1,\n                    x: `${-ribbon.driftX}%`,\n                    y: `${-ribbon.driftY}%`,\n                  }}\n                  animate={{\n                    opacity: [1, 0.65],\n                    rotate: [-ribbon.swayDegrees, ribbon.swayDegrees],\n                    scaleY: [1, 1.15],\n                    x: [`${-ribbon.driftX}%`, `${ribbon.driftX}%`],\n                    y: [`${-ribbon.driftY}%`, `${ribbon.driftY}%`],\n                  }}\n                  transition={{\n                    ...motionConfig.transition,\n                    delay: ribbon.delay,\n                    duration: motionConfig.driftDuration * ribbon.durationScale,\n                  }}\n                />\n              ) : (\n                <div\n                  data-slot=\"aurora-background-ribbon\"\n                  className={cn(\n                    auroraBackgroundRibbonClasses,\n                    auroraBackgroundStaticRibbonOpacityClasses[\n                      slot % auroraBackgroundStaticRibbonOpacityClasses.length\n                    ],\n                  )}\n                  style={ribbonStyle(slot)}\n                />\n              )}\n            </div>\n          ))}\n        </div>\n        <div\n          data-slot=\"aurora-background-content\"\n          className={auroraBackgroundContentClassNames()}\n        >\n          {children}\n        </div>\n      </div>\n    </MotionConfig>\n  );\n});\n\nAuroraBackground.displayName = \"AuroraBackground\";\n"
    },
    {
      "path": "packages/components/src/components/aurora-background/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/aurora-background/index.ts",
      "content": "export {\n  AuroraBackground,\n  auroraBackgroundClassNames,\n  auroraBackgroundContentClassNames,\n  auroraBackgroundHueShifts,\n  auroraBackgroundLayerClassNames,\n  getAuroraBackgroundGeometry,\n  getAuroraBackgroundMotionConfig,\n  type AuroraBackgroundClassNamesOptions,\n  type AuroraBackgroundContentClassNamesOptions,\n  type AuroraBackgroundDensity,\n  type AuroraBackgroundIntensity,\n  type AuroraBackgroundLayerClassNamesOptions,\n  type AuroraBackgroundMotionConfig,\n  type AuroraBackgroundProps,\n  type AuroraBackgroundRibbon,\n  type AuroraBackgroundSpeed,\n  type AuroraBackgroundTone,\n} from \"./aurora-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"
    }
  ]
}
