{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "vhs-filter",
  "title": "VHS Filter",
  "description": "Hold a scene under analog tape: chroma bleeds sideways off every edge, rows wobble out of line, the bottom of the frame tears on the head switch, and luma noise settles over all of it.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/canvas-presentation"],
  "files": [
    {
      "path": "registry/remocn/vhs-filter/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { AbsoluteFill, random, useCurrentFrame } from \"remotion\";\nimport {\n  type CanvasFilterProps,\n  makeCanvasFilter,\n  makeFilterShader,\n} from \"@/lib/remocn/canvas-presentation\";\n\nexport type VhsFilterProps = {\n  bleed?: number;\n  wobble?: number;\n  noise?: number;\n  intensity?: number;\n};\n\nconst DEFAULTS = {\n  bleed: 1,\n  wobble: 1,\n  noise: 1,\n  intensity: 1,\n};\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nuniform sampler2D u_scene;\nuniform float u_frame;\nuniform float u_time;\nuniform float u_bleed;\nuniform float u_wobble;\nuniform float u_noise;\nuniform float u_intensity;\n\nin vec2 v_uv;\nout vec4 outColor;\n\nconst float PI = 3.141592653589793;\nconst float LINES = 486.0;\nconst int TAPS = 6;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(float x) {\n  float i = floor(x);\n  float f = fract(x);\n  float a = hash(vec2(i, 7.0));\n  float b = hash(vec2(i + 1.0, 7.0));\n  return mix(a, b, f * f * (3.0 - 2.0 * f));\n}\n\nvec3 rgb2yiq(vec3 c) {\n  return vec3(\n    dot(c, vec3(0.299, 0.587, 0.114)),\n    dot(c, vec3(0.5959, -0.2746, -0.3213)),\n    dot(c, vec3(0.2115, -0.5227, 0.3112))\n  );\n}\n\nvec3 yiq2rgb(vec3 c) {\n  return vec3(\n    c.x + 0.956 * c.y + 0.619 * c.z,\n    c.x - 0.272 * c.y - 0.647 * c.z,\n    c.x - 1.106 * c.y + 1.703 * c.z\n  );\n}\n\nvoid main() {\n  float line = floor(v_uv.y * LINES);\n  float clock = mod(u_frame, 64.0);\n\n  float drift =\n    sin(v_uv.y * 37.0 + u_time * 2.3) * 0.55 +\n    sin(v_uv.y * 96.0 - u_time * 1.1) * 0.25;\n  float jitter = valueNoise(line * 0.61 + u_time * 24.0) - 0.5;\n  float wobble = (drift * 0.35 + jitter * 0.9) * 0.007 * u_wobble * u_intensity;\n\n  float band = smoothstep(0.955, 0.994, v_uv.y);\n  float tear = valueNoise(line * 2.7 + u_time * 43.0) - 0.5;\n  float headSwitch = band * tear * 0.085 * u_intensity;\n\n  vec2 uv = vec2(v_uv.x + wobble + headSwitch, v_uv.y);\n  vec4 base = texture(u_scene, uv);\n  float luma = rgb2yiq(base.rgb).x;\n\n  float smear = u_bleed * 0.02 * u_intensity;\n  vec2 chroma = vec2(0.0);\n  float total = 0.0;\n  for (int i = 0; i < TAPS; i++) {\n    float t = float(i) / float(TAPS - 1);\n    float weight = 1.0 - t * 0.72;\n    vec3 tap = texture(u_scene, vec2(uv.x - smear * t, uv.y)).rgb;\n    chroma += rgb2yiq(tap).yz * weight;\n    total += weight;\n  }\n\n  vec3 color = yiq2rgb(vec3(luma, chroma / total));\n\n  float scan = 0.94 + 0.06 * cos(v_uv.y * LINES * PI);\n  color *= mix(1.0, scan, u_intensity);\n\n  float grain =\n    hash(vec2(v_uv.x * 640.0 + clock * 13.0, line + clock * 5.0)) - 0.5;\n  color += grain * 0.07 * u_noise * u_intensity;\n\n  color = mix(color, vec3(luma), band * 0.55 * u_intensity);\n  color += band * (hash(vec2(line, clock * 3.0)) - 0.5) * 0.26 * u_intensity;\n\n  outColor = vec4(clamp(color, 0.0, 1.0), base.a);\n}`;\n\nconst shader = makeFilterShader<VhsFilterProps>(\n  FRAGMENT_SHADER,\n  ({ gl, uniform, passedProps }) => {\n    const {\n      bleed = DEFAULTS.bleed,\n      wobble = DEFAULTS.wobble,\n      noise = DEFAULTS.noise,\n      intensity = DEFAULTS.intensity,\n    } = passedProps;\n    gl.uniform1f(uniform(\"u_bleed\"), bleed);\n    gl.uniform1f(uniform(\"u_wobble\"), wobble);\n    gl.uniform1f(uniform(\"u_noise\"), noise);\n    gl.uniform1f(uniform(\"u_intensity\"), intensity);\n  },\n);\n\nconst VhsFilterFallback: React.FC<VhsFilterProps & CanvasFilterProps> = ({\n  children,\n  bleed = DEFAULTS.bleed,\n  wobble = DEFAULTS.wobble,\n  noise = DEFAULTS.noise,\n  intensity = DEFAULTS.intensity,\n}) => {\n  const frame = useCurrentFrame();\n  const shift =\n    (random(`vhs-wobble-${frame}`) - 0.5) * 3.4 * wobble * intensity;\n  const flicker =\n    1 + (random(`vhs-flicker-${frame}`) - 0.5) * 0.07 * noise * intensity;\n  const smear = 3 + bleed * 5;\n\n  return (\n    <AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          translate: `${shift}px`,\n          filter: `saturate(${1 + bleed * 0.3}) contrast(1.04) brightness(${flicker}) blur(${0.3 * intensity}px)`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          translate: `${shift - smear}px`,\n          filter: \"saturate(2.2) blur(1.4px)\",\n          mixBlendMode: \"screen\",\n          opacity: 0.24 * bleed * intensity,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background:\n            \"repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.22) 0px, rgba(0, 0, 0, 0.22) 1px, transparent 1px, transparent 3px)\",\n          opacity: intensity,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          insetInline: 0,\n          bottom: 0,\n          height: \"3.6%\",\n          translate: `${shift * 3}px`,\n          background: `rgba(228, 228, 228, ${0.13 * intensity})`,\n          mixBlendMode: \"screen\",\n        }}\n      />\n    </AbsoluteFill>\n  );\n};\n\nexport const VhsFilter = makeCanvasFilter<VhsFilterProps>({\n  shader,\n  fallback: VhsFilterFallback,\n});\n",
      "type": "registry:component",
      "target": "components/remocn/vhs-filter.tsx"
    }
  ],
  "type": "registry:component"
}
