{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caret-wipe",
  "title": "Caret Wipe",
  "description": "Sweep a typing caret across the frame, wiping the outgoing scene into the next.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/caret-wipe/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport type React from \"react\";\nimport { AbsoluteFill, Easing, interpolate } from \"remotion\";\n\nconst clampOpts = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\nexport type CaretWipeProps = {\n  /** Sweep direction. `right` types the screen left→right; `left` reverses it. */\n  direction?: \"left\" | \"right\";\n  /** The caret's color — the remocn lime by default. */\n  caretColor?: string;\n  /** Caret bar width in px. */\n  caretWidth?: number;\n  /** Caret bar height as a fraction of frame height (0–1), vertically centered. */\n  caretHeight?: number;\n};\n\n// ---------------------------------------------------------------------------\n// caret-wipe — a typographic scene transition.\n//\n// A single tall caret (the remocn text cursor) sweeps across the frame in one\n// eased pass. The caret IS the wipe boundary: the region it has PASSED shows\n// the incoming scene (freshly \"typed in\" — it settles down and un-blurs just\n// behind the caret), while the region it has NOT reached still shows the\n// outgoing scene (being \"backspaced\" — it lifts and blurs as it's consumed).\n//\n// Both scenes share whatever backdrop sits behind the TransitionSeries, so the\n// complementary clips never expose a seam. Pure frame-driven interpolate — no\n// CSS keyframes — so it seeks correctly and renders identically every time.\n// ---------------------------------------------------------------------------\nconst CaretWipePresentation: React.FC<\n  TransitionPresentationComponentProps<CaretWipeProps>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const {\n    direction = \"right\",\n    caretColor = \"#C3E88D\",\n    caretWidth = 3,\n    caretHeight = 0.5,\n  } = passedProps;\n\n  const entering = presentationDirection === \"entering\";\n  const p = presentationProgress;\n  const goingRight = direction === \"right\";\n\n  // Eased sweep so the caret decelerates as it lands the last column.\n  const eased = interpolate(p, [0, 1], [0, 1], {\n    ...clampOpts,\n    easing: Easing.bezier(0.65, 0, 0.35, 1),\n  });\n  // Caret position as a percent of frame width; the wipe boundary.\n  const caretX = goingRight ? eased * 100 : (1 - eased) * 100;\n\n  if (!entering) {\n    // Outgoing: keep only the column the caret hasn't reached yet, and let it\n    // lift + blur as it's consumed (backspaced).\n    const keep = goingRight\n      ? `inset(0 0 0 ${caretX}%)` // keep the right band [caretX, 100]\n      : `inset(0 ${100 - caretX}% 0 0)`; // keep the left band [0, caretX]\n    return (\n      <AbsoluteFill\n        style={{\n          clipPath: keep,\n          WebkitClipPath: keep,\n          transform: `translateY(${interpolate(p, [0, 1], [0, -6], clampOpts)}px)`,\n          filter: `blur(${interpolate(p, [0.1, 1], [0, 4], clampOpts)}px)`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n    );\n  }\n\n  // Incoming: reveal only the column the caret has already passed, and let it\n  // settle down + sharpen just behind the caret (typed in).\n  const reveal = goingRight\n    ? `inset(0 ${100 - caretX}% 0 0)` // reveal the left band [0, caretX]\n    : `inset(0 0 0 ${caretX}%)`; // reveal the right band [caretX, 100]\n\n  // The caret fades in at the very start and out at the very end so it never\n  // pops on/off at the transition's frame edges.\n  const caretOpacity = interpolate(\n    p,\n    [0, 0.08, 0.92, 1],\n    [0, 1, 1, 0],\n    clampOpts,\n  );\n\n  return (\n    <AbsoluteFill>\n      {/* Freshly typed content, clipped to the passed column. */}\n      <AbsoluteFill\n        style={{\n          clipPath: reveal,\n          WebkitClipPath: reveal,\n          transform: `translateY(${interpolate(p, [0, 1], [3, 0], clampOpts)}px)`,\n          filter: `blur(${interpolate(p, [0, 0.8], [2, 0], clampOpts)}px)`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n\n      {/* The caret — drawn on top, unclipped, only in the entering pass so it\n          appears exactly once. */}\n      <AbsoluteFill style={{ pointerEvents: \"none\", opacity: caretOpacity }}>\n        <div\n          style={{\n            position: \"absolute\",\n            left: `${caretX}%`,\n            top: `${(1 - caretHeight) * 50}%`,\n            height: `${caretHeight * 100}%`,\n            width: caretWidth,\n            transform: \"translateX(-50%)\",\n            background: caretColor,\n            borderRadius: caretWidth,\n            boxShadow: `0 0 18px ${caretColor}, 0 0 6px ${caretColor}`,\n          }}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nexport function caretWipe(\n  props: CaretWipeProps = {},\n): TransitionPresentation<CaretWipeProps> {\n  return {\n    component: CaretWipePresentation,\n    props,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caret-wipe.tsx"
    }
  ],
  "type": "registry:component"
}
