{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Ribbons-JS-CSS",
	"title": "Ribbons",
	"description": "Flowing responsive ribbons/cursor trail driven by physics and pointer motion.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "Ribbons.css",
			"target": "@components/Ribbons.css",
			"content": ".ribbons-container {\n  width: 100%;\n  height: 100%;\n  position: relative;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "Ribbons.jsx",
			"content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Transform, Vec3, Color, Polyline } from 'ogl';\n\nimport './Ribbons.css';\n\nconst Ribbons = ({\n  colors = ['#FC8EAC'],\n  baseSpring = 0.03,\n  baseFriction = 0.9,\n  baseThickness = 30,\n  offsetFactor = 0.05,\n  maxAge = 500,\n  pointCount = 50,\n  speedMultiplier = 0.6,\n  enableFade = false,\n  enableShaderEffect = false,\n  effectAmplitude = 2,\n  backgroundColor = [0, 0, 0, 0]\n}) => {\n  const containerRef = useRef(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true });\n    const gl = renderer.gl;\n    if (Array.isArray(backgroundColor) && backgroundColor.length === 4) {\n      gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]);\n    } else {\n      gl.clearColor(0, 0, 0, 0);\n    }\n\n    gl.canvas.style.position = 'absolute';\n    gl.canvas.style.top = '0';\n    gl.canvas.style.left = '0';\n    gl.canvas.style.width = '100%';\n    gl.canvas.style.height = '100%';\n    container.appendChild(gl.canvas);\n\n    const scene = new Transform();\n    const lines = [];\n\n    const vertex = `\n      precision highp float;\n      \n      attribute vec3 position;\n      attribute vec3 next;\n      attribute vec3 prev;\n      attribute vec2 uv;\n      attribute float side;\n      \n      uniform vec2 uResolution;\n      uniform float uDPR;\n      uniform float uThickness;\n      uniform float uTime;\n      uniform float uEnableShaderEffect;\n      uniform float uEffectAmplitude;\n      \n      varying vec2 vUV;\n      \n      vec4 getPosition() {\n          vec4 current = vec4(position, 1.0);\n          vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);\n          vec2 nextScreen = next.xy * aspect;\n          vec2 prevScreen = prev.xy * aspect;\n          vec2 tangent = normalize(nextScreen - prevScreen);\n          vec2 normal = vec2(-tangent.y, tangent.x);\n          normal /= aspect;\n          normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0));\n          float dist = length(nextScreen - prevScreen);\n          normal *= smoothstep(0.0, 0.02, dist);\n          float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);\n          float pixelWidth = current.w * pixelWidthRatio;\n          normal *= pixelWidth * uThickness;\n          current.xy -= normal * side;\n          if(uEnableShaderEffect > 0.5) {\n            current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude;\n          }\n          return current;\n      }\n      \n      void main() {\n          vUV = uv;\n          gl_Position = getPosition();\n      }\n    `;\n\n    const fragment = `\n      precision highp float;\n      uniform vec3 uColor;\n      uniform float uOpacity;\n      uniform float uEnableFade;\n      varying vec2 vUV;\n      void main() {\n          float fadeFactor = 1.0;\n          if(uEnableFade > 0.5) {\n              fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y);\n          }\n          gl_FragColor = vec4(uColor, uOpacity * fadeFactor);\n      }\n    `;\n\n    function resize() {\n      const width = container.clientWidth;\n      const height = container.clientHeight;\n      renderer.setSize(width, height);\n      lines.forEach(line => line.polyline.resize());\n    }\n    window.addEventListener('resize', resize);\n\n    const center = (colors.length - 1) / 2;\n    colors.forEach((color, index) => {\n      const spring = baseSpring + (Math.random() - 0.5) * 0.05;\n      const friction = baseFriction + (Math.random() - 0.5) * 0.05;\n      const thickness = baseThickness + (Math.random() - 0.5) * 3;\n      const mouseOffset = new Vec3(\n        (index - center) * offsetFactor + (Math.random() - 0.5) * 0.01,\n        (Math.random() - 0.5) * 0.1,\n        0\n      );\n\n      const line = {\n        spring,\n        friction,\n        mouseVelocity: new Vec3(),\n        mouseOffset\n      };\n\n      const count = pointCount;\n      const points = [];\n      for (let i = 0; i < count; i++) {\n        points.push(new Vec3());\n      }\n      line.points = points;\n\n      line.polyline = new Polyline(gl, {\n        points,\n        vertex,\n        fragment,\n        uniforms: {\n          uColor: { value: new Color(color) },\n          uThickness: { value: thickness },\n          uOpacity: { value: 1.0 },\n          uTime: { value: 0.0 },\n          uEnableShaderEffect: { value: enableShaderEffect ? 1.0 : 0.0 },\n          uEffectAmplitude: { value: effectAmplitude },\n          uEnableFade: { value: enableFade ? 1.0 : 0.0 }\n        }\n      });\n      line.polyline.mesh.setParent(scene);\n      lines.push(line);\n    });\n\n    resize();\n\n    const mouse = new Vec3();\n    function updateMouse(e) {\n      let x, y;\n      const rect = container.getBoundingClientRect();\n      if (e.changedTouches && e.changedTouches.length) {\n        x = e.changedTouches[0].clientX - rect.left;\n        y = e.changedTouches[0].clientY - rect.top;\n      } else {\n        x = e.clientX - rect.left;\n        y = e.clientY - rect.top;\n      }\n      const width = container.clientWidth;\n      const height = container.clientHeight;\n      mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0);\n    }\n    container.addEventListener('mousemove', updateMouse);\n    container.addEventListener('touchstart', updateMouse);\n    container.addEventListener('touchmove', updateMouse);\n\n    const tmp = new Vec3();\n    let frameId;\n    let lastTime = performance.now();\n    function update() {\n      frameId = requestAnimationFrame(update);\n      const currentTime = performance.now();\n      const dt = currentTime - lastTime;\n      lastTime = currentTime;\n\n      lines.forEach(line => {\n        tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring);\n        line.mouseVelocity.add(tmp).multiply(line.friction);\n        line.points[0].add(line.mouseVelocity);\n\n        for (let i = 1; i < line.points.length; i++) {\n          if (isFinite(maxAge) && maxAge > 0) {\n            const segmentDelay = maxAge / (line.points.length - 1);\n            const alpha = Math.min(1, (dt * speedMultiplier) / segmentDelay);\n            line.points[i].lerp(line.points[i - 1], alpha);\n          } else {\n            line.points[i].lerp(line.points[i - 1], 0.9);\n          }\n        }\n        if (line.polyline.mesh.program.uniforms.uTime) {\n          line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001;\n        }\n        line.polyline.updateGeometry();\n      });\n\n      renderer.render({ scene });\n    }\n    update();\n\n    return () => {\n      window.removeEventListener('resize', resize);\n      container.removeEventListener('mousemove', updateMouse);\n      container.removeEventListener('touchstart', updateMouse);\n      container.removeEventListener('touchmove', updateMouse);\n      cancelAnimationFrame(frameId);\n      if (gl.canvas && gl.canvas.parentNode === container) {\n        container.removeChild(gl.canvas);\n      }\n    };\n  }, [\n    colors,\n    baseSpring,\n    baseFriction,\n    baseThickness,\n    offsetFactor,\n    maxAge,\n    pointCount,\n    speedMultiplier,\n    enableFade,\n    enableShaderEffect,\n    effectAmplitude,\n    backgroundColor\n  ]);\n\n  return <div ref={containerRef} className=\"ribbons-container\" />;\n};\n\nexport default Ribbons;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}