{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "paper-tear",
  "title": "Paper Tear",
  "description": "A jagged rip runs down the frame, then both halves peel apart on a spring with white fibrous torn edges, revealing the scene underneath.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/paper-tear/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  spring,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type PaperTearProps = {\n  /** Strength of the impact: shake, flash and debris. 0 turns them off. */\n  impact?: number;\n  /** Seed for the shape of the rip. */\n  seed?: number | string;\n  /** Color of the fibrous torn edge. */\n  edgeColor?: string;\n  /** How jagged the rip is, 0 to 1. */\n  roughness?: number;\n  /** Shadow the torn halves cast on the incoming scene. */\n  shadowColor?: string;\n};\n\nexport const paperTearDefaults: Required<PaperTearProps> = {\n  impact: 1,\n  seed: 7,\n  edgeColor: \"#FFFFFF\",\n  roughness: 0.5,\n  shadowColor: \"rgba(0, 0, 0, 0.45)\",\n};\n\n// Timeline, as shares of the transition: the paper strains, then the rip\n// tears through the whole frame in a couple of frames (the impact), then the\n// halves are flung apart.\nconst STRAIN = 0.2;\nconst IMPACT = 0.28;\nconst SEGMENTS = 44;\nconst DEBRIS = 28;\nconst ripEase = Easing.in(Easing.quad);\nconst clamp = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\ntype TearPoint = { x: number; y: number; left: number; right: number };\n\nfunction buildTear(\n  seed: number | string,\n  roughness: number,\n  width: number,\n  height: number,\n): TearPoint[] {\n  const r = Math.min(1, Math.max(0, roughness));\n  const unit = Math.min(width, height);\n  const tilt = (random(`tear-tilt-${seed}`) - 0.5) * width * 0.08;\n  const phaseA = random(`tear-pa-${seed}`) * Math.PI * 2;\n  const phaseB = random(`tear-pb-${seed}`) * Math.PI * 2;\n  const fiber = unit * (0.008 + r * 0.012);\n  const points: TearPoint[] = [];\n  for (let i = 0; i <= SEGMENTS; i++) {\n    const t = i / SEGMENTS;\n    const wander =\n      Math.sin(t * Math.PI * 2.2 + phaseA) * width * 0.025 +\n      Math.sin(t * Math.PI * 5.3 + phaseB) * width * 0.012;\n    // Alternate the jitter sign so the rip zig-zags instead of drifting.\n    const zig = (i % 2 === 0 ? 1 : -1) * random(`tear-z-${seed}-${i}`);\n    points.push({\n      x:\n        width / 2 +\n        tilt * (t - 0.5) +\n        wander * (0.4 + r) +\n        zig * unit * 0.022 * r,\n      y: -unit * 0.05 + t * (height + unit * 0.1),\n      left: fiber * (0.35 + random(`tear-l-${seed}-${i}`) * 1.3),\n      right: fiber * (0.2 + random(`tear-r-${seed}-${i}`) * 0.9),\n    });\n  }\n  return points;\n}\n\ntype Shard = {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  spin: number;\n  size: number;\n  shape: string;\n};\n\n// Paper scraps blown out of the seam at the moment of impact.\nfunction buildDebris(\n  seed: number | string,\n  tear: TearPoint[],\n  unit: number,\n): Shard[] {\n  return Array.from({ length: DEBRIS }, (_, i) => {\n    const r = (k: string) => random(`tear-debris-${seed}-${i}-${k}`);\n    const at = tear[Math.floor(r(\"at\") * tear.length)];\n    const side = r(\"side\") < 0.5 ? -1 : 1;\n    const corners = Array.from({ length: 4 }, (_, k) => {\n      const a = (k / 4) * Math.PI * 2 + r(`a${k}`) * 1.2;\n      const d = 0.55 + r(`d${k}`) * 0.45;\n      return `${(50 + Math.cos(a) * 50 * d).toFixed(0)}% ${(50 + Math.sin(a) * 50 * d).toFixed(0)}%`;\n    });\n    return {\n      x: at.x,\n      y: at.y,\n      vx: side * unit * (0.25 + r(\"vx\") * 0.9),\n      vy: -unit * (0.1 + r(\"vy\") * 0.55),\n      spin: (r(\"spin\") - 0.5) * 1440,\n      size: unit * (0.008 + r(\"size\") ** 2 * 0.026),\n      shape: `polygon(${corners.join(\",\")})`,\n    };\n  });\n}\n\n// Damped camera jolt that starts at the impact.\nfunction shakeAt(p: number, unit: number, impact: number) {\n  const t = p - IMPACT;\n  if (t < 0 || impact <= 0) return { x: 0, y: 0, amp: 0 };\n  const decay = Math.exp(-t * 11);\n  const amp = unit * 0.03 * impact * decay;\n  return {\n    x: Math.sin(t * 95) * amp,\n    y: Math.cos(t * 120 + 0.6) * amp * 0.7,\n    amp,\n  };\n}\n\nconst px = (x: number, y: number) => `${x}px ${y}px`;\n\nconst PaperTearPresentation: React.FC<\n  TransitionPresentationComponentProps<PaperTearProps>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const { width, height, fps } = useVideoConfig();\n  const { seed, edgeColor, roughness, shadowColor, impact } = {\n    ...paperTearDefaults,\n    ...passedProps,\n  };\n  const tear = useMemo(\n    () => buildTear(seed, roughness, width, height),\n    [seed, roughness, width, height],\n  );\n  const unit = Math.min(width, height);\n  const debris = useMemo(\n    () => buildDebris(seed, tear, unit),\n    [seed, tear, unit],\n  );\n  const p = presentationProgress;\n  const shake = shakeAt(p, unit, impact);\n\n  if (presentationDirection === \"entering\") {\n    // The next scene is punched toward camera by the hit and settles back.\n    const scale = interpolate(p, [IMPACT, 1], [1 + 0.1 * impact, 1], {\n      ...clamp,\n      easing: Easing.out(Easing.cubic),\n    });\n    return (\n      <AbsoluteFill\n        style={{\n          transform: `translate(${shake.x}px, ${shake.y}px) scale(${scale})`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n    );\n  }\n\n  // Strain: the sheet trembles and leans toward camera before it gives way.\n  const strain = interpolate(p, [0, STRAIN, IMPACT], [0, 1, 1], {\n    ...clamp,\n    easing: Easing.in(Easing.cubic),\n  });\n  const tremble =\n    p < IMPACT ? Math.sin(p * 260) * unit * 0.004 * strain * impact : 0;\n  const lean = 1 + 0.025 * strain * (p < IMPACT ? 1 : 0);\n  const front =\n    -unit * 0.1 +\n    ripEase(interpolate(p, [STRAIN, IMPACT], [0, 1], clamp)) *\n      (height + unit * 0.25);\n  // The torn fibres open up a short distance behind the running rip front.\n  const open = (y: number) =>\n    interpolate(front - y, [0, unit * 0.12], [0, 1], clamp);\n\n  const pad = unit;\n  const edge = tear.map((t) => px(t.x, t.y));\n  const leftInk = tear.map((t) => px(t.x - t.left * open(t.y), t.y));\n  const rightInk = tear.map((t) => px(t.x + t.right * open(t.y), t.y));\n  const leftOuter = [px(-pad, height + pad), px(-pad, -pad)];\n  const rightOuter = [px(width + pad, height + pad), px(width + pad, -pad)];\n  const poly = (pts: string[]) => `polygon(${pts.join(\",\")})`;\n  const leftPaper = poly([...edge, ...leftOuter]);\n  const rightPaper = poly([...edge, ...rightOuter]);\n  const leftPrint = poly([...leftInk, ...leftOuter]);\n  const rightPrint = poly([...rightInk, ...rightOuter]);\n\n  const peelFrames = 30;\n  // A stiff spring right after the impact flings the halves apart.\n  const peel = spring({\n    frame: Math.max(0, (p - IMPACT) / (1 - IMPACT)) * peelFrames,\n    fps,\n    durationInFrames: peelFrames,\n    config: { damping: 15, stiffness: 150, mass: 0.8 },\n  });\n  const maxX = Math.max(...tear.map((t) => t.x));\n  const minX = Math.min(...tear.map((t) => t.x));\n  // The inner edges swing toward camera as the halves part, so travel gets\n  // headroom for the perspective growth. Pivoting on the outer edges keeps\n  // the frame border sealed (a flat rotate would expose the top or bottom).\n  const leftTravel = (maxX + unit * 0.12) * 1.3;\n  const rightTravel = (width - minX + unit * 0.12) * 1.3;\n  const curl = peel * 24;\n  const lens = `perspective(${unit * 2.6}px)`;\n  // Grow the shadow from zero so the closed rip shows no dark seam.\n  const cast = interpolate(p, [IMPACT, IMPACT + 0.1], [0, 1], clamp);\n  const shadow = `drop-shadow(0 ${unit * 0.012 * cast}px ${unit * 0.04 * cast}px ${shadowColor})`;\n\n  const half = (\n    paper: string,\n    print: string,\n    transform: string,\n    origin: string,\n  ) => (\n    <AbsoluteFill\n      style={{\n        transform,\n        transformOrigin: origin,\n        filter: cast > 0 ? shadow : undefined,\n      }}\n    >\n      <AbsoluteFill style={{ clipPath: paper, backgroundColor: edgeColor }} />\n      <AbsoluteFill style={{ clipPath: print }}>{children}</AbsoluteFill>\n    </AbsoluteFill>\n  );\n\n  // Flash along the seam and scraps of paper blown out of it.\n  const flash =\n    impact > 0\n      ? interpolate(p, [IMPACT - 0.03, IMPACT, IMPACT + 0.14], [0, 1, 0], {\n          ...clamp,\n        }) * Math.min(1, impact)\n      : 0;\n  const seam = tear.map((t) => `${t.x},${t.y}`).join(\" \");\n  const age = p - IMPACT;\n  const gravity = unit * 2.6;\n  // Debris time runs in \"seconds\" of the transition so it reads physical.\n  const dt = age * 1.1;\n\n  return (\n    <AbsoluteFill\n      style={{\n        zIndex: 2,\n        // Overscan just enough that the jolt never exposes the frame edge.\n        transform: `translate(${shake.x + tremble}px, ${shake.y}px) scale(${\n          lean + (2.2 * shake.amp) / unit\n        })`,\n      }}\n    >\n      {front < height + unit * 0.05 ? (\n        // Below the running rip the sheet is still whole. Cover the seam with\n        // an intact copy so the split halves never show an anti-aliased hairline.\n        <AbsoluteFill\n          style={{\n            clipPath: poly([\n              px(-pad, front),\n              px(width + pad, front),\n              px(width + pad, height + pad),\n              px(-pad, height + pad),\n            ]),\n            zIndex: 1,\n          }}\n        >\n          {children}\n        </AbsoluteFill>\n      ) : null}\n      {half(\n        leftPaper,\n        leftPrint,\n        `${lens} translateX(${-peel * leftTravel}px) rotateY(${-curl}deg)`,\n        \"0% 50%\",\n      )}\n      {half(\n        rightPaper,\n        rightPrint,\n        `${lens} translateX(${peel * rightTravel}px) rotateY(${curl}deg)`,\n        \"100% 50%\",\n      )}\n      {flash > 0.01 ? (\n        <svg\n          width={width}\n          height={height}\n          viewBox={`0 0 ${width} ${height}`}\n          style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n          aria-hidden\n        >\n          <polyline\n            points={seam}\n            fill=\"none\"\n            stroke={edgeColor}\n            strokeOpacity={flash * 0.35}\n            strokeWidth={unit * 0.06 * flash}\n            strokeLinejoin=\"round\"\n          />\n          <polyline\n            points={seam}\n            fill=\"none\"\n            stroke={edgeColor}\n            strokeOpacity={flash}\n            strokeWidth={unit * 0.012}\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      ) : null}\n      {impact > 0 && age > 0\n        ? debris.map((shard, i) => {\n            const fade = interpolate(age, [0.35, 0.6], [1, 0], clamp);\n            if (fade <= 0) return null;\n            const x = shard.x + shard.vx * dt * impact;\n            const y =\n              shard.y + shard.vy * dt * impact + 0.5 * gravity * dt * dt;\n            const size = shard.size * (0.5 + 0.5 * Math.min(1, impact));\n            return (\n              <div\n                key={i}\n                style={{\n                  position: \"absolute\",\n                  left: x - size / 2,\n                  top: y - size / 2,\n                  width: size,\n                  height: size,\n                  backgroundColor: edgeColor,\n                  clipPath: shard.shape,\n                  opacity: fade,\n                  transform: `rotate(${shard.spin * dt}deg)`,\n                }}\n              />\n            );\n          })\n        : null}\n    </AbsoluteFill>\n  );\n};\n\nexport function paperTear(\n  props: PaperTearProps = {},\n): TransitionPresentation<PaperTearProps> {\n  return { component: PaperTearPresentation, props };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/paper-tear.tsx"
    }
  ],
  "type": "registry:component"
}
