{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tv-power-off",
  "title": "TV Power Off",
  "description": "End a scene the way a tube does: the picture collapses into a glowing horizontal line, the line pinches to a phosphor dot, the dot decays, and the frame is left dead black.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/canvas-presentation"],
  "files": [
    {
      "path": "registry/remocn/tv-power-off/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { AbsoluteFill, useCurrentFrame } from \"remotion\";\nimport {\n  type CanvasFilterProps,\n  makeCanvasFilter,\n  makeFilterShader,\n} from \"@/lib/remocn/canvas-presentation\";\n\nexport type TvPowerOffProps = {\n  durationInFrames?: number;\n  delay?: number;\n  gain?: number;\n  afterglow?: number;\n  phosphor?: string;\n};\n\nconst DEFAULTS = {\n  durationInFrames: 18,\n  delay: 0,\n  gain: 1,\n  afterglow: 1,\n  phosphor: \"#d8ecff\",\n};\n\nconst COLLAPSE_END = 0.6;\nconst PINCH_END = 0.82;\nconst MIN_SQUASH = 0.0055;\nconst MIN_PINCH = 0.004;\nconst CONCENTRATION = 0.3;\nconst DECAY_CURVE = 2.6;\nconst BLACKOUT_IN = 0.06;\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nuniform sampler2D u_scene;\nuniform vec2 u_resolution;\nuniform float u_aspect;\nuniform float u_progress;\nuniform float u_gain;\nuniform float u_afterglow;\nuniform vec3 u_phosphor;\n\nin vec2 v_uv;\nout vec4 outColor;\n\nconst int TAPS = 32;\nconst int COLUMN_TAPS = 8;\nconst float GOLDEN = 0.6180339887;\nconst float COLLAPSE_END = ${COLLAPSE_END.toFixed(4)};\nconst float PINCH_END = ${PINCH_END.toFixed(4)};\nconst float MIN_SQUASH = ${MIN_SQUASH.toFixed(5)};\nconst float MIN_PINCH = ${MIN_PINCH.toFixed(5)};\nconst float CONCENTRATION = ${CONCENTRATION.toFixed(4)};\nconst float DECAY_CURVE = ${DECAY_CURVE.toFixed(4)};\nconst float BLACKOUT_IN = ${BLACKOUT_IN.toFixed(4)};\n\nvoid main() {\n  float p = u_progress;\n\n  if (p <= 0.0) {\n    outColor = texture(u_scene, v_uv);\n    return;\n  }\n  if (p >= 1.0) {\n    outColor = vec4(0.0, 0.0, 0.0, 1.0);\n    return;\n  }\n\n  float collapse = clamp(p / COLLAPSE_END, 0.0, 1.0);\n  float pinch = clamp((p - COLLAPSE_END) / (PINCH_END - COLLAPSE_END), 0.0, 1.0);\n  float decayed = clamp((p - PINCH_END) / (1.0 - PINCH_END), 0.0, 1.0);\n\n  vec2 scale = vec2(pow(MIN_PINCH, pinch), pow(MIN_SQUASH, collapse));\n  vec2 centered = (v_uv - 0.5) / scale + 0.5;\n  vec2 span = 1.0 / (scale * u_resolution);\n\n  vec3 sum = vec3(0.0);\n  for (int i = 0; i < TAPS; i++) {\n    float fi = float(i);\n    vec2 at = centered + vec2(\n      (fract(fi * GOLDEN + 0.5) - 0.5) * span.x,\n      ((fi + 0.5) / float(TAPS) - 0.5) * span.y\n    );\n    vec2 inside = step(vec2(0.0), at) * step(at, vec2(1.0));\n    sum += texture(u_scene, at).rgb * inside.x * inside.y;\n  }\n\n  float concentrate = pow(1.0 / (scale.x * scale.y), CONCENTRATION);\n  vec3 light =\n    (sum / float(TAPS)) * (1.0 + (concentrate - 1.0) * max(u_gain, 0.0));\n\n  float glow = max(u_afterglow, 0.0);\n  float bleed = smoothstep(0.2, 0.8, collapse) * (1.0 - pinch) * glow;\n\n  if (bleed > 0.0) {\n    vec3 column = vec3(0.0);\n    float cx = clamp(centered.x, 0.0, 1.0);\n    for (int i = 0; i < COLUMN_TAPS; i++) {\n      column +=\n        texture(u_scene, vec2(cx, (float(i) + 0.5) / float(COLUMN_TAPS))).rgb;\n    }\n    column /= float(COLUMN_TAPS);\n\n    float dy = v_uv.y - 0.5;\n    float reach = 0.02 + 0.03 * collapse;\n    light +=\n      column *\n      mix(vec3(1.0), u_phosphor, 0.65) *\n      exp(-(dy * dy) / (reach * reach)) *\n      bleed *\n      0.9;\n  }\n\n  if (pinch > 0.0) {\n    vec2 d = (v_uv - 0.5) * vec2(u_aspect, 1.0);\n    float radius = max(0.045 * glow, 0.0005);\n    light +=\n      u_phosphor *\n      exp(-dot(d, d) / (radius * radius)) *\n      smoothstep(0.0, 0.5, pinch) *\n      1.15;\n  }\n\n  light *= pow(max(1.0 - decayed, 0.0001), DECAY_CURVE);\n\n  float blackout = smoothstep(0.0, BLACKOUT_IN, p);\n  outColor = vec4(\n    clamp(light, 0.0, 1.0),\n    mix(texture(u_scene, v_uv).a, 1.0, blackout)\n  );\n}`;\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\nfunction toRgb(hex: string): [number, number, number] {\n  const value = hex.replace(\"#\", \"\");\n  const full =\n    value.length === 3\n      ? value\n          .split(\"\")\n          .map((c) => c + c)\n          .join(\"\")\n      : value;\n  const int = Number.parseInt(full, 16);\n  if (Number.isNaN(int)) return [1, 1, 1];\n  return [\n    ((int >> 16) & 255) / 255,\n    ((int >> 8) & 255) / 255,\n    (int & 255) / 255,\n  ];\n}\n\nfunction powerProgress(props: TvPowerOffProps, frame: number): number {\n  const {\n    durationInFrames = DEFAULTS.durationInFrames,\n    delay = DEFAULTS.delay,\n  } = props;\n  return clamp((frame - delay) / Math.max(1, durationInFrames), 0, 1);\n}\n\nconst shader = makeFilterShader<TvPowerOffProps>(\n  FRAGMENT_SHADER,\n  ({ gl, uniform, width, height, frame, passedProps }) => {\n    const {\n      gain = DEFAULTS.gain,\n      afterglow = DEFAULTS.afterglow,\n      phosphor = DEFAULTS.phosphor,\n    } = passedProps;\n    gl.uniform1f(uniform(\"u_progress\"), powerProgress(passedProps, frame));\n    gl.uniform2f(uniform(\"u_resolution\"), width, height);\n    gl.uniform1f(uniform(\"u_gain\"), gain);\n    gl.uniform1f(uniform(\"u_afterglow\"), afterglow);\n    gl.uniform3fv(uniform(\"u_phosphor\"), toRgb(phosphor));\n  },\n);\n\nconst TvPowerOffFallback: React.FC<TvPowerOffProps & CanvasFilterProps> = (\n  props,\n) => {\n  const frame = useCurrentFrame();\n  const {\n    children,\n    gain = DEFAULTS.gain,\n    afterglow = DEFAULTS.afterglow,\n    phosphor = DEFAULTS.phosphor,\n  } = props;\n\n  const p = powerProgress(props, frame);\n  const collapse = clamp(p / COLLAPSE_END, 0, 1);\n  const pinch = clamp((p - COLLAPSE_END) / (PINCH_END - COLLAPSE_END), 0, 1);\n  const decayed = clamp((p - PINCH_END) / (1 - PINCH_END), 0, 1);\n\n  const squash = MIN_SQUASH ** collapse;\n  const pinchX = MIN_PINCH ** pinch;\n  const concentrate = (1 / (squash * pinchX)) ** CONCENTRATION;\n  const fade = (1 - decayed) ** DECAY_CURVE;\n\n  return (\n    <AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background: \"#000000\",\n          opacity: clamp(p / BLACKOUT_IN, 0, 1),\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          scale: `${pinchX} ${squash}`,\n          filter: `brightness(${1 + (concentrate - 1) * Math.max(gain, 0)})`,\n          opacity: fade,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(circle at 50% 50%, ${phosphor} 0%, transparent 55%)`,\n          opacity: clamp(pinch * afterglow, 0, 1) * fade,\n        }}\n      />\n    </AbsoluteFill>\n  );\n};\n\nexport const TvPowerOff = makeCanvasFilter<TvPowerOffProps>({\n  shader,\n  fallback: TvPowerOffFallback,\n});\n",
      "type": "registry:component",
      "target": "components/remocn/tv-power-off.tsx"
    }
  ],
  "type": "registry:component"
}
