{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "light-streaks-background",
  "type": "registry:ui",
  "title": "LightStreaksBackground",
  "description": "Animated landing-page background: blurred diagonal light streaks sweeping across the surface on staggered seeded loops. 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/light-streaks-background/light-streaks-background.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/light-streaks-background/light-streaks-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, seededRange } from \"../../utils/seeded-random\";\n\nexport type LightStreaksBackgroundDensity = \"sparse\" | \"normal\" | \"dense\";\nexport type LightStreaksBackgroundIntensity = \"faint\" | \"subtle\" | \"bold\";\nexport type LightStreaksBackgroundSpeed = \"slow\" | \"normal\" | \"fast\";\nexport type LightStreaksBackgroundTone = \"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 LightStreaksBackgroundProps extends MotionSafeDivProps {\n  animate?: boolean;\n  children?: ReactNode;\n  density?: LightStreaksBackgroundDensity;\n  intensity?: LightStreaksBackgroundIntensity;\n  seed?: number;\n  speed?: LightStreaksBackgroundSpeed;\n  tone?: LightStreaksBackgroundTone;\n}\n\nexport interface LightStreaksBackgroundMotionConfig {\n  animateEnabled: boolean;\n  reducedMotion: boolean;\n  sweepDuration: number;\n  transition: Transition;\n}\n\nexport interface LightStreaksBackgroundStreak {\n  delay: number;\n  left: number;\n  repeatDelay: number;\n}\n\nconst lightStreaksBackgroundSpeedDurations: Record<\n  LightStreaksBackgroundSpeed,\n  number\n> = {\n  slow: 10,\n  normal: 7,\n  fast: 4.5,\n};\n\nexport function getLightStreaksBackgroundMotionConfig(\n  speed: LightStreaksBackgroundSpeed,\n  reducedMotion = false,\n): LightStreaksBackgroundMotionConfig {\n  if (reducedMotion) {\n    return {\n      animateEnabled: false,\n      reducedMotion: true,\n      sweepDuration: 0,\n      transition: { duration: 0 },\n    };\n  }\n\n  const sweepDuration = lightStreaksBackgroundSpeedDurations[speed];\n\n  return {\n    animateEnabled: true,\n    reducedMotion: false,\n    sweepDuration,\n    transition: {\n      duration: sweepDuration,\n      ease: \"easeInOut\",\n      repeat: Infinity,\n    },\n  };\n}\n\nconst lightStreaksBackgroundStreakCounts: Record<\n  LightStreaksBackgroundDensity,\n  number\n> = {\n  sparse: 3,\n  normal: 4,\n  dense: 5,\n};\n\nfunction roundTo(value: number, decimals: number): number {\n  const factor = 10 ** decimals;\n  return Math.round(value * factor) / factor;\n}\n\nexport function getLightStreaksBackgroundGeometry(\n  seed: number,\n  density: LightStreaksBackgroundDensity,\n): LightStreaksBackgroundStreak[] {\n  const random = mulberry32(seed);\n\n  return Array.from(\n    { length: lightStreaksBackgroundStreakCounts[density] },\n    () => ({\n      left: Math.round(seededRange(random, 5, 80)),\n      delay: roundTo(seededRange(random, 0, 5), 2),\n      repeatDelay: roundTo(seededRange(random, 1, 4), 2),\n    }),\n  );\n}\n\nconst lightStreaksBackgroundRootClasses = \"relative isolate overflow-hidden\";\n\nconst lightStreaksBackgroundLayerClasses =\n  \"pointer-events-none absolute inset-0 select-none\";\n\nconst lightStreaksBackgroundContentClasses = \"relative z-10\";\n\n// Each streak rail is oversized vertically and statically rotated so the\n// blurred bar reads as a diagonal glare; the bar itself only ever animates\n// transform and opacity.\nconst lightStreaksBackgroundRailClasses =\n  \"absolute -inset-y-1/4 w-24 -rotate-[20deg]\";\n\nconst lightStreaksBackgroundStreakClasses =\n  \"h-full w-full bg-gradient-to-r from-transparent via-current to-transparent blur-2xl\";\n\nconst lightStreaksBackgroundToneIntensityClasses: Record<\n  LightStreaksBackgroundTone,\n  Record<LightStreaksBackgroundIntensity, string>\n> = {\n  foreground: {\n    faint: \"text-foreground/20 dark:text-foreground/25\",\n    subtle: \"text-foreground/30 dark:text-foreground/40\",\n    bold: \"text-foreground/40 dark:text-foreground/55\",\n  },\n  muted: {\n    faint: \"text-muted-foreground/25 dark:text-muted-foreground/30\",\n    subtle: \"text-muted-foreground/35 dark:text-muted-foreground/45\",\n    bold: \"text-muted-foreground/50 dark:text-muted-foreground/60\",\n  },\n  primary: {\n    faint: \"text-primary/25 dark:text-primary/30\",\n    subtle: \"text-primary/40 dark:text-primary/50\",\n    bold: \"text-primary/55 dark:text-primary/65\",\n  },\n};\n\nexport interface LightStreaksBackgroundClassNamesOptions {\n  className?: string;\n}\n\nexport function lightStreaksBackgroundClassNames({\n  className,\n}: LightStreaksBackgroundClassNamesOptions = {}) {\n  return cn(lightStreaksBackgroundRootClasses, className);\n}\n\nexport interface LightStreaksBackgroundLayerClassNamesOptions {\n  className?: string;\n  intensity?: LightStreaksBackgroundIntensity;\n  tone?: LightStreaksBackgroundTone;\n}\n\nexport function lightStreaksBackgroundLayerClassNames({\n  className,\n  intensity = \"subtle\",\n  tone = \"muted\",\n}: LightStreaksBackgroundLayerClassNamesOptions = {}) {\n  return cn(\n    lightStreaksBackgroundLayerClasses,\n    lightStreaksBackgroundToneIntensityClasses[tone][intensity],\n    className,\n  );\n}\n\nexport interface LightStreaksBackgroundContentClassNamesOptions {\n  className?: string;\n}\n\nexport function lightStreaksBackgroundContentClassNames({\n  className,\n}: LightStreaksBackgroundContentClassNamesOptions = {}) {\n  return cn(lightStreaksBackgroundContentClasses, 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 sweeps 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(streak: LightStreaksBackgroundStreak): CSSProperties {\n  return { left: `${streak.left}%` };\n}\n\nexport const LightStreaksBackground = forwardRef(\n  function LightStreaksBackground(\n    {\n      animate = true,\n      children,\n      className,\n      density = \"normal\",\n      intensity = \"subtle\",\n      seed = 1,\n      speed = \"normal\",\n      tone = \"muted\",\n      ...props\n    }: LightStreaksBackgroundProps,\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 = getLightStreaksBackgroundMotionConfig(\n      speed,\n      reducedMotion,\n    );\n    const streaks = useMemo(\n      () => getLightStreaksBackgroundGeometry(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=\"light-streaks-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={lightStreaksBackgroundClassNames({ className })}\n        >\n          <div\n            aria-hidden=\"true\"\n            data-slot=\"light-streaks-background-layer\"\n            className={lightStreaksBackgroundLayerClassNames({\n              intensity,\n              tone,\n            })}\n          >\n            {running\n              ? streaks.map((streak, index) => (\n                  <div\n                    key={index}\n                    className={lightStreaksBackgroundRailClasses}\n                    style={railStyle(streak)}\n                  >\n                    <motionElement.div\n                      data-slot=\"light-streaks-background-streak\"\n                      className={lightStreaksBackgroundStreakClasses}\n                      initial={{ opacity: 0, x: \"-250%\" }}\n                      animate={{ opacity: [0, 1, 0], x: [\"-250%\", \"250%\"] }}\n                      transition={{\n                        ...motionConfig.transition,\n                        delay: streak.delay,\n                        repeatDelay: streak.repeatDelay,\n                      }}\n                    />\n                  </div>\n                ))\n              : streaks.slice(0, 1).map((streak, index) => (\n                  <div\n                    key={index}\n                    className={lightStreaksBackgroundRailClasses}\n                    style={railStyle(streak)}\n                  >\n                    <div\n                      data-slot=\"light-streaks-background-streak\"\n                      className={cn(\n                        lightStreaksBackgroundStreakClasses,\n                        \"opacity-40\",\n                      )}\n                    />\n                  </div>\n                ))}\n          </div>\n          <div\n            data-slot=\"light-streaks-background-content\"\n            className={lightStreaksBackgroundContentClassNames()}\n          >\n            {children}\n          </div>\n        </div>\n      </MotionConfig>\n    );\n  },\n);\n\nLightStreaksBackground.displayName = \"LightStreaksBackground\";\n"
    },
    {
      "path": "packages/components/src/components/light-streaks-background/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/light-streaks-background/index.ts",
      "content": "export {\n  LightStreaksBackground,\n  getLightStreaksBackgroundGeometry,\n  getLightStreaksBackgroundMotionConfig,\n  lightStreaksBackgroundClassNames,\n  lightStreaksBackgroundContentClassNames,\n  lightStreaksBackgroundLayerClassNames,\n  type LightStreaksBackgroundClassNamesOptions,\n  type LightStreaksBackgroundContentClassNamesOptions,\n  type LightStreaksBackgroundDensity,\n  type LightStreaksBackgroundIntensity,\n  type LightStreaksBackgroundLayerClassNamesOptions,\n  type LightStreaksBackgroundMotionConfig,\n  type LightStreaksBackgroundProps,\n  type LightStreaksBackgroundSpeed,\n  type LightStreaksBackgroundStreak,\n  type LightStreaksBackgroundTone,\n} from \"./light-streaks-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"
    }
  ]
}
