{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pov-setup",
  "title": "POV Setup",
  "description": "A 'POV:' setup line slides in word by word, then a serif italic punchline lands.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/pov-setup/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { loadFont } from \"@remotion/google-fonts/Sora\";\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: sora } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serifItalic } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PovSetupProps {\n  /** The format label. */\n  label?: string;\n  /** The setup line; words slide in one by one. */\n  setup?: string;\n  /** Lead-in of the punchline, set in the serif italic. */\n  lead?: string;\n  /** The key word of the punchline. */\n  punch?: string;\n  /** Kept for the shared draft API; the colour comes from the ground. */\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  /** gradients.sh preset id for the ground. */\n  ground?: GshPresetId;\n  /** Overrides for the ground shader. */\n  groundParams?: Partial<GradientParams>;\n  speed?: number;\n}\n\nconst BEATS = {\n  pov: 0,\n  shrink: 6,\n  setup: 10,\n  wordGap: 3,\n  wordLen: 12,\n  punch: 54,\n  lift: 54,\n  rise: 56,\n} as const;\n\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst LIGHT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CAMERA = Easing.bezier(0.87, 0, 0.13, 1);\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 CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    stage: \"#0A0807\",\n    ink: \"#F4F1EC\",\n    preset: \"hearth\" as GshPresetId,\n    params: undefined as Partial<GradientParams> | undefined,\n    veilFrom: 0.9,\n    veilTo: 0.18,\n  },\n  light: {\n    stage: \"#F2EEE8\",\n    ink: \"#17120E\",\n    preset: \"paper-crease\" as GshPresetId,\n    params: {\n      colors: [\"#FF8A3A\", \"#FFC79A\", \"#F6E2CF\", \"#5A2410\"],\n      background: \"#F2EEE8\",\n    } as Partial<GradientParams>,\n    veilFrom: 0.78,\n    veilTo: 0.12,\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 shake(d: number, amp: number, seed: number) {\n  if (d < 0 || d > 8) return { x: 0, y: 0 };\n  const k = (1 - d / 8) ** 2;\n  return {\n    x: amp * k * Math.sin(d * 2.7 + seed),\n    y: amp * 0.7 * k * Math.cos(d * 3.3 + seed * 1.7),\n  };\n}\n\nexport function PovSetup({\n  label = \"POV:\",\n  setup = \"you shipped the launch video before standup\",\n  lead = \"and it\",\n  punch = \"slaps.\",\n  theme = \"dark\",\n  ground,\n  groundParams,\n  speed = 1,\n}: PovSetupProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const tall = H > W;\n  const base = tall ? W * 0.75 : H;\n  const u = Math.min(W / 1280, H / 720);\n\n  const words = setup.split(/\\s+/).filter(Boolean);\n\n  const pov = spring({ frame: t - BEATS.pov, fps, config: CHIP });\n  const shrink = spring({ frame: t - BEATS.shrink, fps, config: HEAVY });\n  const lift = spring({ frame: t - BEATS.lift, fps, config: HEAVY });\n  const p = spring({ frame: t - BEATS.punch, fps, config: CARD });\n  const punchOn = t >= BEATS.punch;\n\n  const bump = Math.max(\n    0.5 * kick(t - BEATS.pov - 4),\n    1.2 * kick(t - BEATS.punch - 4),\n  );\n  const hit = shake(t - BEATS.punch - 4, 8 * u, 2.3);\n  const creep = interpolate(t, [BEATS.setup, BEATS.punch + 70], [1, 1.035], {\n    ...CLAMP,\n    easing: CAMERA,\n  });\n  const camScale = creep * (1 + 0.03 * bump);\n\n  const povScale = 1.4 - 0.4 * pov;\n  const povBlur = Math.max(0, 1 - pov) * 10;\n  const punchScale = 1.5 - 0.5 * p;\n  const punchBlur = Math.max(0, 1 - p) * 14;\n  const punchTilt = -4 * (1 - p);\n  const stepBack = LIGHT(ramp(t, BEATS.punch, 12));\n  const setupOpacity = 1 - 0.55 * stepBack;\n\n  const rise = LIGHT(ramp(t, BEATS.rise, 30));\n  const veil = c.veilFrom + (c.veilTo - c.veilFrom) * rise;\n  const groundShift = (1 - rise) * 0.14 * H;\n\n  const labelFs = 0.075 * base;\n  const setupFs = (tall ? 0.105 : 0.1) * base;\n  const punchFs = 0.3 * base;\n  const left = (tall ? 0.08 : 0.085) * W;\n  const setupWidth = W - 2 * left;\n\n  const POV_BIG = 3.2;\n  const labelGap = 0.035 * base;\n  const punchGap = 0.045 * base;\n  const setupLines = Math.max(\n    1,\n    Math.ceil((setup.length * 0.54 * setupFs) / setupWidth),\n  );\n  const punchLines = tall ? 2 : 1;\n  const labelH = labelFs + labelGap;\n  const setupH = setupLines * setupFs * 1.12;\n  const punchH = punchLines * punchFs * 1.02 + punchGap;\n  const top0 = (H - labelFs * POV_BIG) / 2;\n  const top1 = (H - labelH - setupH) / 2;\n  const top2 = (H - labelH - setupH - punchH) / 2;\n  const blockTop = top0 + (top1 - top0) * shrink + (top2 - top1) * lift;\n  const labelScale = POV_BIG - (POV_BIG - 1) * shrink;\n\n  return (\n    <AbsoluteFill style={{ background: c.stage, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          top: -0.3 * H,\n          height: 1.55 * H,\n          transform: `translateY(${groundShift - 0.25 * H}px) scale(1.08)`,\n        }}\n      >\n        <GshGround\n          preset={ground ?? c.preset}\n          params={groundParams ?? c.params}\n          timeOffset={4}\n          timeScale={0.45}\n        />\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background: `linear-gradient(180deg, ${c.stage} 0%, ${c.stage} 38%, transparent 100%)`,\n          opacity: veil,\n        }}\n      />\n      <AbsoluteFill style={{ background: c.stage, opacity: veil * 0.55 }} />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `translate(${hit.x}px, ${hit.y}px) scale(${camScale})`,\n          transformOrigin: \"30% 50%\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left,\n            right: left,\n            top: blockTop,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"flex-start\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: sora,\n              fontWeight: 400,\n              fontSize: labelFs,\n              lineHeight: 1,\n              letterSpacing: \"-0.01em\",\n              color: c.ink,\n              transform: `scale(${povScale * labelScale})`,\n              transformOrigin: \"0% 0%\",\n              height: labelFs,\n              filter: povBlur > 0.2 ? `blur(${povBlur * u}px)` : undefined,\n              opacity: ramp(t, BEATS.pov - 1, 2),\n              marginBottom: labelGap,\n            }}\n          >\n            {label}\n          </div>\n\n          <div\n            style={{\n              fontFamily: sora,\n              fontWeight: 300,\n              fontSize: setupFs,\n              lineHeight: 1.12,\n              letterSpacing: \"-0.035em\",\n              color: c.ink,\n              maxWidth: setupWidth,\n              opacity: setupOpacity,\n            }}\n          >\n            {words.map((w, i) => {\n              const at = BEATS.setup + i * BEATS.wordGap;\n              const k = ENTER(ramp(t, at, BEATS.wordLen));\n              return (\n                <span\n                  key={i}\n                  style={{\n                    display: \"inline-block\",\n                    overflow: \"hidden\",\n                    verticalAlign: \"top\",\n                    paddingBottom: \"0.14em\",\n                    marginBottom: \"-0.14em\",\n                    marginRight: i < words.length - 1 ? \"0.24em\" : 0,\n                  }}\n                >\n                  <span\n                    style={{\n                      display: \"inline-block\",\n                      transform: `translateY(${(1 - k) * 105}%)`,\n                      opacity: ramp(t, at, 4),\n                    }}\n                  >\n                    {w}\n                  </span>\n                </span>\n              );\n            })}\n          </div>\n\n          <div\n            style={{\n              height: punchLines * punchFs * 1.02,\n              marginTop: punchGap,\n              display: \"flex\",\n              alignItems: \"flex-end\",\n            }}\n          >\n            {punchOn ? (\n              <div\n                style={{\n                  display: \"flex\",\n                  flexDirection: tall ? \"column\" : \"row\",\n                  alignItems: tall ? \"flex-start\" : \"baseline\",\n                  whiteSpace: \"nowrap\",\n                  transform: `rotate(${punchTilt}deg) scale(${punchScale})`,\n                  transformOrigin: \"0% 70%\",\n                  filter:\n                    punchBlur > 0.2 ? `blur(${punchBlur * u}px)` : undefined,\n                  opacity: ramp(t, BEATS.punch, 2),\n                  color: c.ink,\n                }}\n              >\n                <span\n                  style={{\n                    fontFamily: serifItalic,\n                    fontStyle: \"italic\",\n                    fontWeight: 400,\n                    fontSize: punchFs * 1.04,\n                    lineHeight: 1,\n                    letterSpacing: \"-0.03em\",\n                    marginRight: \"0.14em\",\n                  }}\n                >\n                  {lead}\n                </span>\n                <span\n                  style={{\n                    fontFamily: sora,\n                    fontWeight: 400,\n                    fontSize: punchFs,\n                    lineHeight: 1,\n                    letterSpacing: \"-0.06em\",\n                    paddingRight: \"0.04em\",\n                  }}\n                >\n                  {punch}\n                </span>\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/pov-setup.tsx"
    }
  ],
  "type": "registry:component"
}
