{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "duet-split",
  "title": "Duet Split",
  "description": "A split screen contrasts a slow grey side with a crisp gradient side: '1 day' versus '$0'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/duet-split/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/BricolageGrotesque\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst { fontFamily: sans } = loadSans(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nexport interface DuetRow {\n  before: string;\n  after: string;\n}\n\nexport interface DuetSplitProps {\n  beforeLabel?: string;\n  afterLabel?: string;\n  rows?: DuetRow[];\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n  speed?: number;\n}\n\nconst LABEL_AT = 6;\nconst ROW_AT = 18;\nconst ROW_GAP = 16;\nconst TRICKLE = 3;\nconst GLYPH_LEN = 14;\nconst PUSH_DELAY = 24;\nconst PUSH_LEN = 16;\n\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst MORPH = { damping: 200 };\nconst ENTER = Easing.bezier(0.22, 1, 0.36, 1);\nconst MOVE = Easing.bezier(0.65, 0, 0.35, 1);\nconst EXIT = Easing.bezier(0.55, 0, 1, 0.45);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    ground: \"film-blue\" as GshPresetId,\n    base: \"#03070F\",\n    slab: \"#161618\",\n    seam: \"rgba(255,255,255,0.10)\",\n    ink: \"#F4F5F8\",\n    muted: \"rgba(244,245,248,0.62)\",\n    grey: \"#6B6A70\",\n    greyLabel: \"#7E7D83\",\n    scrim: \"3,7,15\",\n    scrimA: 0.5,\n    shadow: \"0 1px 18px rgba(3,7,15,0.45)\",\n  },\n  light: {\n    ground: \"paper-crease\" as GshPresetId,\n    base: \"#EEECE8\",\n    slab: \"#D9D6CF\",\n    seam: \"rgba(0,0,0,0.10)\",\n    ink: \"#121318\",\n    muted: \"rgba(18,19,24,0.6)\",\n    grey: \"#A29F97\",\n    greyLabel: \"#8D8A82\",\n    scrim: \"238,236,232\",\n    scrimA: 0.45,\n    shadow: \"none\",\n  },\n} as const;\n\nfunction ramp(t: number, from: number, len: number) {\n  return interpolate(t, [from, from + len], [0, 1], CLAMP);\n}\n\nfunction kick(d: number) {\n  if (d < 0) return 0;\n  if (d < 2) return d / 2;\n  const k = Math.max(0, 1 - (d - 2) / 8);\n  return k * k;\n}\n\nfunction Wobbly({\n  text,\n  at,\n  t,\n  fontSize,\n  color,\n  seed,\n}: {\n  text: string;\n  at: number;\n  t: number;\n  fontSize: number;\n  color: string;\n  seed: number;\n}) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        fontFamily: serif,\n        fontWeight: 400,\n        fontSize,\n        lineHeight: 1,\n        letterSpacing: \"-0.01em\",\n        color,\n        whiteSpace: \"pre\",\n      }}\n    >\n      {[...text].map((ch, k) => {\n        const p = MOVE(ramp(t, at + k * TRICKLE, GLYPH_LEN));\n        const ph = seed * 2.3 + k * 1.1;\n        const wy = Math.sin(t * 0.13 + ph) * 0.035 * fontSize;\n        const rot = Math.sin(t * 0.09 + ph * 0.7) * 3;\n        return (\n          <span\n            key={k}\n            style={{\n              display: \"inline-block\",\n              opacity: p,\n              transform: `translateY(${(1 - p) * 0.35 * fontSize + wy}px) rotate(${rot * (0.4 + 0.6 * p)}deg)`,\n              filter:\n                p < 0.98 ? `blur(${(1 - p) * 0.05 * fontSize}px)` : undefined,\n            }}\n          >\n            {ch}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction Label({\n  text,\n  fs,\n  color,\n  dot,\n}: {\n  text: string;\n  fs: number;\n  color: string;\n  dot: string;\n}) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: fs * 0.5,\n        fontFamily: sans,\n        fontWeight: 400,\n        fontSize: fs,\n        lineHeight: 1,\n        letterSpacing: \"-0.005em\",\n        color,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      <div\n        style={{\n          width: fs * 0.36,\n          height: fs * 0.36,\n          borderRadius: fs,\n          background: dot,\n        }}\n      />\n      {text}\n    </div>\n  );\n}\n\nexport function DuetSplit({\n  beforeLabel = \"Before\",\n  afterLabel = \"With remocn\",\n  rows = [\n    { before: \"3 weeks\", after: \"1 day\" },\n    { before: \"an agency\", after: \"you\" },\n    { before: \"$12K\", after: \"$0\" },\n  ],\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  ground,\n  groundParams,\n  speed = 1,\n}: DuetSplitProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const tall = H > W;\n\n  const sp = (at: number, config: object, durationInFrames?: number) =>\n    spring({\n      frame: t - at,\n      fps,\n      config,\n      ...(durationInFrames ? { durationInFrames } : {}),\n    });\n\n  const open = sp(-5, HEAVY);\n  const lastRowAt = ROW_AT + (rows.length - 1) * ROW_GAP;\n  const pushAt = lastRowAt + PUSH_DELAY;\n  const push = sp(pushAt, MORPH, PUSH_LEN);\n  const logoAt = pushAt + PUSH_LEN - 4;\n  const pLogo = sp(logoAt, CARD);\n\n  const fs = tall ? 0.135 * W : 0.15 * H;\n  const gap = fs * 1.18;\n  const labelFs = tall ? 0.024 * H : 0.034 * H;\n  const margin = tall ? 0.07 * W : 0.05 * W;\n\n  const half = tall ? H / 2 : W / 2;\n  const slabStyle = tall\n    ? {\n        left: 0,\n        top: 0,\n        width: W,\n        height: H / 2,\n        transform: `translateY(${-(1 - open) * half - push * half}px)`,\n      }\n    : {\n        left: 0,\n        top: 0,\n        width: W / 2,\n        height: H,\n        transform: `translate(${-push * half}px, ${-(1 - open) * H}px)`,\n      };\n\n  const beforeC = tall\n    ? { x: W / 2, y: H * 0.27 }\n    : { x: W * 0.25, y: H * 0.54 };\n  const afterStart = tall\n    ? { x: W / 2, y: H * 0.77 }\n    : { x: W * 0.75, y: H * 0.54 };\n  const afterEnd = tall ? { x: W / 2, y: H * 0.46 } : { x: W / 2, y: H * 0.45 };\n  const ac = {\n    x: afterStart.x + (afterEnd.x - afterStart.x) * push,\n    y: afterStart.y + (afterEnd.y - afterStart.y) * push,\n  };\n  const grow = 1 + (tall ? 0.16 : 0.2) * push;\n  const rowY = (i: number) => (i - (rows.length - 1) / 2) * gap;\n\n  const bump = Math.max(\n    ...rows.map((_, i) => 0.6 * kick(t - ROW_AT - i * ROW_GAP - 3)),\n    kick(t - pushAt - PUSH_LEN + 4),\n  );\n  const camScale =\n    1 + 0.016 * bump + interpolate(t, [0, 160], [0, 0.02], CLAMP);\n\n  const afterOpen = sp(2, HEAVY);\n  const labelIn = ENTER(ramp(t, LABEL_AT, 12));\n  const labelOut = EXIT(ramp(t, pushAt, 8));\n\n  const crisp = (text: string, at: number) => {\n    const p = sp(at, CHIP);\n    return (\n      <div\n        style={{\n          fontFamily: sans,\n          fontWeight: 300,\n          fontSize: fs,\n          lineHeight: 1,\n          letterSpacing: \"-0.045em\",\n          whiteSpace: \"nowrap\",\n          color: c.ink,\n          textShadow: c.shadow,\n          opacity: ENTER(ramp(t, at, 3)),\n          transform: `translateY(${(1 - p) * 0.22 * fs}px) scale(${1.1 - 0.1 * p})`,\n        }}\n      >\n        {text}\n      </div>\n    );\n  };\n\n  const place = (x: number, y: number, child: React.ReactNode, key: string) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: y,\n        width: 0,\n        height: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {child}\n    </div>\n  );\n\n  const afterLabelPos = tall\n    ? { left: margin, top: H / 2 + margin }\n    : { left: W / 2 + margin, top: margin * 1.2 };\n  const scrimAt = tall\n    ? `50% ${(ac.y / H) * 100}%`\n    : `${(ac.x / W) * 100}% 54%`;\n\n  return (\n    <AbsoluteFill style={{ background: c.base, overflow: \"hidden\" }}>\n      <GshGround\n        preset={groundParams ? undefined : (ground ?? c.ground)}\n        params={groundParams}\n        timeOffset={theme === \"dark\" ? 4 : 6}\n        timeScale={0.5}\n      />\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(${tall ? \"80% 34%\" : \"40% 60%\"} at ${scrimAt}, rgba(${c.scrim},${c.scrimA}) 0%, rgba(${c.scrim},0) 100%)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{ transform: `scale(${camScale})`, transformOrigin: \"50% 50%\" }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            ...afterLabelPos,\n            opacity: labelIn * (1 - labelOut),\n            transform: `translateY(${(1 - labelIn) * labelFs * 0.6}px)`,\n          }}\n        >\n          <Label text={afterLabel} fs={labelFs} color={c.ink} dot={accent} />\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            transformOrigin: `${ac.x}px ${ac.y}px`,\n            transform: `translateY(${(1 - afterOpen) * 0.08 * H}px) scale(${grow})`,\n          }}\n        >\n          {rows.map((r, i) => {\n            const at = ROW_AT + i * ROW_GAP;\n            return t >= at\n              ? place(ac.x, ac.y + rowY(i), crisp(r.after, at), `a-${i}`)\n              : null;\n          })}\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            ...slabStyle,\n            background: c.slab,\n            boxShadow: tall\n              ? `inset 0 -1px 0 ${c.seam}`\n              : `inset -1px 0 0 ${c.seam}`,\n            overflow: \"hidden\",\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: margin,\n              top: tall ? margin : margin * 1.2,\n              opacity: labelIn,\n              transform: `translateY(${(1 - labelIn) * labelFs * 0.6}px)`,\n            }}\n          >\n            <Label\n              text={beforeLabel}\n              fs={labelFs}\n              color={c.greyLabel}\n              dot={c.grey}\n            />\n          </div>\n          {rows.map((r, i) =>\n            place(\n              tall ? W / 2 : W * 0.25,\n              beforeC.y + rowY(i),\n              <Wobbly\n                text={r.before}\n                at={ROW_AT + i * ROW_GAP}\n                t={t}\n                fontSize={fs * 1.04}\n                color={c.grey}\n                seed={i + 1}\n              />,\n              `b-${i}`,\n            ),\n          )}\n        </div>\n      </AbsoluteFill>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          bottom: tall ? 0.08 * H : 0.085 * H,\n          display: \"flex\",\n          justifyContent: \"center\",\n          opacity: ENTER(ramp(t, logoAt, 6)),\n          transform: `translateY(${(1 - pLogo) * 0.05 * H}px)`,\n        }}\n      >\n        <RemocnLogo fontSize={tall ? 0.04 * H : 0.055 * H} color={c.ink} />\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/duet-split.tsx"
    }
  ],
  "type": "registry:component"
}
