{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FluxLines-TS-CSS",
	"title": "FluxLines",
	"description": "Glowing flux lines tracing a churning magnetic fluid. The cursor magnet reveals and curls them.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "FluxLines.css",
			"target": "@components/FluxLines.css",
			"content": ".flux-lines-container {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.flux-lines-canvas {\n  position: absolute;\n  inset: 0;\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "FluxLines.tsx",
			"content": "'use client';\n\nimport { useEffect, useRef, type CSSProperties, type HTMLAttributes } from 'react';\nimport './FluxLines.css';\n\nexport type FluxLinesFlow = 'up' | 'down' | 'left' | 'right';\n\nexport interface FluxLinesProps extends HTMLAttributes<HTMLDivElement> {\n  colors?: string[];\n  speed?: number;\n  scale?: number;\n  turbulence?: number;\n  fluidity?: number;\n  rimWidth?: number;\n  sharpness?: number;\n  shimmer?: number;\n  glow?: number;\n  lineWidth?: number;\n  flowDirection?: FluxLinesFlow;\n  opacity?: number;\n  mouseInteraction?: boolean;\n  mouseStrength?: number;\n  mouseRadius?: number;\n  mouseDampening?: number;\n  clickSwirl?: boolean;\n  intro?: boolean;\n  lightMode?: boolean;\n  transparent?: boolean;\n  backgroundColor?: string;\n  paused?: boolean;\n  dpr?: number;\n  mixBlendMode?: CSSProperties['mixBlendMode'];\n}\n\ntype Settings = {\n  colors: string[];\n  speed: number;\n  scale: number;\n  turbulence: number;\n  fluidity: number;\n  rimWidth: number;\n  sharpness: number;\n  shimmer: number;\n  glow: number;\n  lineWidth: number;\n  flow: number[];\n  opacity: number;\n  mouseInteraction: boolean;\n  mouseStrength: number;\n  mouseRadius: number;\n  mouseDampening: number;\n  clickSwirl: boolean;\n  intro: boolean;\n  lightMode: boolean;\n  transparent: boolean;\n  backgroundColor?: string;\n  paused: boolean;\n  dpr?: number;\n};\n\ntype Program = { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation | null> };\n\nconst MAX_COLORS = 8;\nconst INTRO_SECONDS = 1.8;\nconst FLOW: Record<string, number[]> = { up: [0, 1], down: [0, -1], left: [-1, 0], right: [1, 0] };\n\nconst VERTEX = `#version 300 es\nin vec2 aPosition;\nvoid main() {\n  gl_Position = vec4(aPosition, 0.0, 1.0);\n}`;\n\nconst FIELD = `\nfloat hash(vec3 p3) {\n  p3 = fract(p3 * 0.1031);\n  p3 += dot(p3, p3.zyx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat smin(float a, float b, float k) {\n  float r = exp2(-a / k) + exp2(-b / k);\n  return -k * log2(r);\n}\n\nfloat sinlerp(float a, float b, float w) {\n  return mix(a, b, (sin(w * 3.14159265 - 3.14159265 / 2.0) + 1.0) / 2.0);\n}\n\nfloat vn(vec2 p, float s, float seed) {\n  vec2 cellp = floor(p / s);\n  vec2 relp = mod(p, s);\n  float g1 = hash(vec3(cellp, seed));\n  float g2 = hash(vec3(cellp.x + 1.0, cellp.y, seed));\n  float g3 = hash(vec3(cellp.x + 1.0, cellp.y + 1.0, seed));\n  float g4 = hash(vec3(cellp.x, cellp.y + 1.0, seed));\n  float bx = sinlerp(g1, g2, relp.x / s);\n  float tx = sinlerp(g4, g3, relp.x / s);\n  return sinlerp(bx, tx, relp.y / s);\n}\n\nfloat dbn(vec2 p, float s, float seed) {\n  float o = s / 2.0;\n  float n0 = vn(p, s, seed);\n  float n1 = vn(p + vec2(o, o), s, seed + 0.1);\n  float n2 = vn(p + vec2(-o, o), s, seed + 0.2);\n  float n3 = vn(p + vec2(o, -o), s, seed + 0.3);\n  float n4 = vn(p + vec2(-o, -o), s, seed + 0.4);\n  return (2.0 * n0 + 1.5 * n1 + 1.25 * n2 + 1.125 * n3 + n4) / 7.0;\n}\n`;\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\nprecision highp int;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform vec2 uFlow;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uTurbulence;\nuniform float uFluidity;\nuniform float uRimWidth;\nuniform float uSharpness;\nuniform float uShimmer;\nuniform float uGlow;\nuniform float uLine;\nuniform float uMaxHalf;\nuniform vec3 uColors[${MAX_COLORS}];\nuniform int uColorCount;\nuniform vec4 uMagnet;\nuniform vec4 uSwirl;\nuniform float uReveal;\nuniform float uLight;\nuniform float uOpaque;\nuniform vec3 uBackground;\nuniform float uOpacity;\nout vec4 outColor;\n${FIELD}\n\nvec3 palette(float h) {\n  float x = clamp(h, 0.0, 1.0) * float(uColorCount - 1);\n  int i = int(floor(x));\n  int j = min(i + 1, uColorCount - 1);\n  return mix(uColors[i], uColors[j], smoothstep(0.0, 1.0, x - float(i)));\n}\n\nvoid main() {\n  float ref = 700.0 / max(uScale, 0.05);\n  vec2 frag = gl_FragCoord.xy;\n  vec2 toMagnet = (frag - uMagnet.xy) / uResolution.y;\n  float pull = uMagnet.z * exp(-dot(toMagnet, toMagnet) / (uMagnet.w * uMagnet.w));\n  if (pull > 0.001) {\n    float angle = pull * 0.55;\n    float c = cos(angle);\n    float s = sin(angle);\n    frag = uMagnet.xy + mat2(c, s, -s, c) * (frag - uMagnet.xy);\n  }\n  if (uSwirl.w > 0.0) {\n    vec2 d = frag - uSwirl.xy;\n    float reach = uSwirl.z;\n    float angle = uSwirl.w * exp(-dot(d, d) / (reach * reach));\n    float c = cos(angle);\n    float s = sin(angle);\n    frag = uSwirl.xy + mat2(c, s, -s, c) * d;\n  }\n  vec2 p = frag / uResolution.y * ref;\n\n  float spd = 200.0 * uSpeed;\n  float t = uTime;\n  vec2 dir = uFlow;\n  vec2 perp = vec2(-dir.y, dir.x);\n  float distort1 = vn(p + perp * (t * spd), 60.0, 10.0) * 50.0 * uTurbulence;\n  float distort2 = vn(p - perp * (t * spd), 120.0, 15.0) * 100.0 * uTurbulence;\n  float peaks = dbn(p + distort1 + dir * (t * spd * 0.5), 40.0, 1.0);\n  float peaks2 = dbn(p + distort2 - dir * (t * spd * 0.5), 40.0, 0.0);\n  float field = smin(peaks, peaks2, max(uFluidity, 0.001));\n\n  float slope = length(vec2(dFdx(field), dFdy(field)));\n  float gap = abs(field - 0.4);\n  float pixels = gap / max(slope, 1e-6);\n  float halfBand = max(uRimWidth, 0.001) * 0.5;\n  float across = max(gap / halfBand, pixels / uMaxHalf);\n  float band = 5.0 * uRimWidth * (1.0 - across);\n  float reveal = smoothstep(0.2, 0.85, pull);\n  float shimmer = vn(p + dir * (t * spd * 0.5), 60.0, 12.0) * uShimmer * (1.0 - reveal * 0.75);\n  float lit = clamp(band - shimmer, 0.0, 1.0);\n  float glow = pow(lit, uSharpness) * uGlow * (1.0 + reveal * 0.25);\n  float core = uLine > 0.0 ? smoothstep(uLine + 0.75, uLine - 0.75, pixels) * max(smoothstep(0.08, 0.6, glow), reveal) : 0.0;\n  float shine = clamp(glow, 0.0, 1.0);\n  float energy = max(shine * shine, core * 0.85) * uReveal;\n\n  float h = clamp(0.5 + (peaks - peaks2) * 0.8, 0.0, 1.0);\n  vec3 tint = palette(h);\n\n  if (uLight > 0.5) {\n    float chroma = max(tint.r, max(tint.g, tint.b)) - min(tint.r, min(tint.g, tint.b));\n    vec3 ink = mix(vec3(0.07, 0.07, 0.08), tint * 0.55, clamp(chroma * 1.6, 0.0, 1.0));\n    float cover = energy * uOpacity;\n    outColor = uOpaque > 0.5 ? vec4(mix(uBackground, ink, cover), 1.0) : vec4(ink * cover, cover);\n    return;\n  }\n\n  vec3 light = tint * energy * uOpacity;\n  outColor = uOpaque > 0.5 ? vec4(uBackground + light, 1.0) : vec4(light, max(light.r, max(light.g, light.b)));\n}`;\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;\n  gl.deleteShader(shader);\n  return null;\n};\n\nconst link = (gl: WebGL2RenderingContext): Program | null => {\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT);\n  const program = gl.createProgram();\n  if (!vertex || !fragment || !program) return null;\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.bindAttribLocation(program, 0, 'aPosition');\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n  const uniforms: Record<string, WebGLUniformLocation | null> = {};\n  const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n  for (let i = 0; i < count; i++) {\n    const info = gl.getActiveUniform(program, i);\n    if (info) uniforms[info.name.replace('[0]', '')] = gl.getUniformLocation(program, info.name);\n  }\n  return { program, uniforms };\n};\n\nconst FluxLines = ({\n  colors = ['#ffffff', '#ffffff', '#ffffff'],\n  speed = 0.5,\n  scale = 1.6,\n  turbulence = 1,\n  fluidity = 0.1,\n  rimWidth = 0.2,\n  sharpness = 2.5,\n  shimmer = 1.5,\n  glow = 2,\n  lineWidth = 1,\n  flowDirection = 'down',\n  opacity = 1,\n  mouseInteraction = true,\n  mouseStrength = 1,\n  mouseRadius = 0.35,\n  mouseDampening = 0.15,\n  clickSwirl = true,\n  intro = true,\n  lightMode = false,\n  transparent = true,\n  backgroundColor,\n  paused = false,\n  dpr,\n  mixBlendMode,\n  className = '',\n  style,\n  ...rest\n}: FluxLinesProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const wakeRef = useRef<(() => void) | null>(null);\n  const settings: Settings = {\n    colors: Array.isArray(colors) && colors.length ? colors.slice(0, MAX_COLORS) : ['#ffffff'],\n    speed,\n    scale: Math.max(0.05, scale),\n    turbulence,\n    fluidity: Math.max(0.001, fluidity),\n    rimWidth: Math.max(0.001, rimWidth),\n    sharpness: Math.max(0.1, sharpness),\n    shimmer: Math.max(0, shimmer),\n    glow: Math.max(0, glow),\n    lineWidth: Math.max(0, lineWidth),\n    flow: FLOW[flowDirection] ?? FLOW.down,\n    opacity: clamp(opacity, 0, 1),\n    mouseInteraction,\n    mouseStrength: Math.max(0, mouseStrength),\n    mouseRadius: Math.max(0.01, mouseRadius),\n    mouseDampening: Math.max(0, mouseDampening),\n    clickSwirl,\n    intro,\n    lightMode,\n    transparent,\n    backgroundColor,\n    paused,\n    dpr\n  };\n  const settingsRef = useRef(settings);\n  settingsRef.current = settings;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const gl = canvas?.getContext('webgl2', {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n      depth: false,\n      stencil: false\n    });\n    if (!container || !canvas || !gl) return undefined;\n\n    const program = link(gl);\n    if (!program) return undefined;\n    const u = program.uniforms;\n\n    const buffer = gl.createBuffer();\n    const vao = gl.createVertexArray();\n    gl.bindVertexArray(vao);\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    gl.enableVertexAttribArray(0);\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n\n    const probe = document.createElement('canvas').getContext('2d', { willReadFrequently: true });\n    const cache = new Map<string, number[]>();\n    const parse = (value: string | undefined, fallback: number[]): number[] => {\n      if (!value) return fallback;\n      const key = String(value);\n      const cached = cache.get(key);\n      if (cached) return cached;\n      let rgb = fallback;\n      if (probe) {\n        probe.clearRect(0, 0, 1, 1);\n        probe.fillStyle = '#000000';\n        probe.fillStyle = key;\n        probe.fillRect(0, 0, 1, 1);\n        const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;\n        rgb = [r / 255, g / 255, b / 255];\n      }\n      cache.set(key, rgb);\n      return rgb;\n    };\n    const palette = new Float32Array(MAX_COLORS * 3);\n\n    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n    const pointer = { x: 0, y: 0, inside: false };\n    const magnet = { x: 0, y: 0, active: 0 };\n    const swirl = { x: 0, y: 0, age: 10 };\n    let time = 0;\n    let reveal = settingsRef.current.intro && !reduce ? 0 : 1;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n\n    const render = () => {\n      const s = settingsRef.current;\n      const ratio = canvas.width / Math.max(1, container.clientWidth);\n      s.colors.forEach((value, index) => palette.set(parse(value, [1, 1, 1]), index * 3));\n      const intro = Math.min(1, reveal);\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.useProgram(program.program);\n      gl.bindVertexArray(vao);\n      gl.uniform2f(u.uResolution, canvas.width, canvas.height);\n      gl.uniform1f(u.uTime, time);\n      gl.uniform2f(u.uFlow, s.flow[0], s.flow[1]);\n      gl.uniform1f(u.uSpeed, s.speed);\n      gl.uniform1f(u.uScale, s.scale);\n      gl.uniform1f(u.uTurbulence, s.turbulence);\n      gl.uniform1f(u.uFluidity, s.fluidity);\n      gl.uniform1f(u.uRimWidth, s.rimWidth);\n      gl.uniform1f(u.uSharpness, s.sharpness);\n      gl.uniform1f(u.uShimmer, s.shimmer);\n      gl.uniform1f(u.uGlow, s.glow);\n      gl.uniform1f(u.uLine, s.lineWidth * ratio * 0.5);\n      gl.uniform1f(u.uMaxHalf, 28 * ratio);\n      gl.uniform3fv(u.uColors, palette);\n      gl.uniform1i(u.uColorCount, s.colors.length);\n      gl.uniform4f(u.uMagnet, magnet.x, magnet.y, magnet.active * s.mouseStrength, s.mouseRadius);\n      const age = swirl.age;\n      const twist = 1.9 * Math.min(1, age / 0.23) * Math.exp(-Math.max(0, age - 0.23) * 1.1);\n      gl.uniform4f(u.uSwirl, swirl.x, swirl.y, canvas.height * 0.22 * (1 + age * 0.5), age < 6 ? twist : 0);\n      gl.uniform1f(u.uReveal, intro * intro * (3 - 2 * intro));\n      gl.uniform1f(u.uLight, s.lightMode ? 1 : 0);\n      gl.uniform1f(u.uOpaque, s.transparent ? 0 : 1);\n      gl.uniform3fv(u.uBackground, parse(s.backgroundColor, s.lightMode ? [1, 1, 1] : [0.0706, 0.0588, 0.0902]));\n      gl.uniform1f(u.uOpacity, s.opacity);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (!visible || document.hidden) return;\n      const s = settingsRef.current;\n      const dt = last ? clamp((now - last) / 1000, 0.0005, 0.05) : 1 / 60;\n      last = now;\n      const motion = s.paused || reduce ? 0 : 1;\n      time += dt * motion;\n      swirl.age += dt;\n      if (s.intro && !reduce && reveal < 1) reveal = Math.min(1, reveal + dt / INTRO_SECONDS);\n      else if (!s.intro || reduce) reveal = 1;\n\n      const engaged = s.mouseInteraction && pointer.inside;\n      const follow = s.mouseDampening > 0 ? 1 - Math.exp(-dt / s.mouseDampening) : 1;\n      if (engaged) {\n        magnet.x += (pointer.x - magnet.x) * follow;\n        magnet.y += (pointer.y - magnet.y) * follow;\n      }\n      const target = engaged ? 1 : 0;\n      magnet.active += (target - magnet.active) * (1 - Math.exp(-dt / 0.3));\n      if (Math.abs(target - magnet.active) < 0.001) magnet.active = target;\n\n      render();\n      const busy = motion > 0 || magnet.active !== target || reveal < 1 || swirl.age < 6;\n      if (busy) raf = requestAnimationFrame(frame);\n      else last = 0;\n    };\n\n    const wake = () => {\n      if (!raf && visible && !document.hidden) raf = requestAnimationFrame(frame);\n    };\n\n    const resize = () => {\n      const s = settingsRef.current;\n      const width = Math.max(1, container.clientWidth);\n      const height = Math.max(1, container.clientHeight);\n      const ratio = Math.min(s.dpr || window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * ratio));\n      canvas.height = Math.max(1, Math.round(height * ratio));\n      render();\n      wake();\n    };\n\n    const locate = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const x = event.clientX - rect.left;\n      const y = event.clientY - rect.top;\n      const ratio = canvas.width / Math.max(1, rect.width);\n      pointer.inside = x >= 0 && y >= 0 && x <= rect.width && y <= rect.height;\n      pointer.x = x * ratio;\n      pointer.y = (rect.height - y) * ratio;\n    };\n\n    const onPointerMove = (event: PointerEvent) => {\n      const wasInside = pointer.inside;\n      locate(event);\n      if (pointer.inside && !wasInside && magnet.active < 0.05) {\n        magnet.x = pointer.x;\n        magnet.y = pointer.y;\n      }\n      wake();\n    };\n\n    const onPointerDown = (event: PointerEvent) => {\n      onPointerMove(event);\n      if (!pointer.inside || !settingsRef.current.clickSwirl || reduce) return;\n      swirl.x = pointer.x;\n      swirl.y = pointer.y;\n      swirl.age = 0;\n      wake();\n    };\n\n    const onPointerUp = (event: PointerEvent) => {\n      if (event.pointerType !== 'mouse') pointer.inside = false;\n    };\n\n    const onPointerLeave = () => {\n      pointer.inside = false;\n    };\n\n    const onVisibility = () => {\n      last = 0;\n      wake();\n    };\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    const intersection = new IntersectionObserver(entries => {\n      visible = entries.some(entry => entry.isIntersecting);\n      if (visible) {\n        last = 0;\n        wake();\n      }\n    });\n    intersection.observe(container);\n    window.addEventListener('pointermove', onPointerMove, { passive: true });\n    window.addEventListener('pointerdown', onPointerDown, { passive: true });\n    window.addEventListener('pointerup', onPointerUp, { passive: true });\n    window.addEventListener('pointercancel', onPointerUp, { passive: true });\n    document.documentElement.addEventListener('pointerleave', onPointerLeave);\n    document.addEventListener('visibilitychange', onVisibility);\n    resize();\n\n    let lastDpr = settingsRef.current.dpr;\n    wakeRef.current = () => {\n      const s = settingsRef.current;\n      if (s.dpr !== lastDpr) {\n        lastDpr = s.dpr;\n        resize();\n        return;\n      }\n      if (!raf) render();\n      wake();\n    };\n\n    return () => {\n      cancelAnimationFrame(raf);\n      wakeRef.current = null;\n      resizeObserver.disconnect();\n      intersection.disconnect();\n      window.removeEventListener('pointermove', onPointerMove);\n      window.removeEventListener('pointerdown', onPointerDown);\n      window.removeEventListener('pointerup', onPointerUp);\n      window.removeEventListener('pointercancel', onPointerUp);\n      document.documentElement.removeEventListener('pointerleave', onPointerLeave);\n      document.removeEventListener('visibilitychange', onVisibility);\n      gl.deleteProgram(program.program);\n      gl.deleteBuffer(buffer);\n      gl.deleteVertexArray(vao);\n      if (!canvas.isConnected) gl.getExtension('WEBGL_lose_context')?.loseContext();\n    };\n  }, []);\n\n  useEffect(() => {\n    wakeRef.current?.();\n  });\n\n  return (\n    <div\n      ref={containerRef}\n      className={`flux-lines-container${className ? ` ${className}` : ''}`}\n      style={mixBlendMode ? { ...style, mixBlendMode } : style}\n      {...rest}\n    >\n      <canvas ref={canvasRef} className=\"flux-lines-canvas\" aria-hidden=\"true\" />\n    </div>\n  );\n};\n\nexport default FluxLines;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}