{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "panel-split",
  "title": "Panel Split",
  "description": "A portfolio page splits into stacked panels of bio, case study and reel.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/panel-split/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont } from \"@remotion/google-fonts/InstrumentSans\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport type { ReactNode } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  Img,\n  interpolate,\n  spring,\n  staticFile,\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 sans = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst serif = loadSerif(\"italic\", { weights: [\"400\"], subsets: [\"latin\"] });\nconst mono = loadMono(\"normal\", { weights: [\"400\"], subsets: [\"latin\"] });\n\nexport interface PanelSplitProps {\n  images?: string[];\n  name?: string;\n  role?: string;\n  headline?: string[];\n  question?: string;\n  body?: string;\n  caption?: string;\n  labels?: string[];\n  theme?: \"dark\" | \"light\";\n  accent?: string;\n  panel?: string;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}\n\nconst THEMES = {\n  dark: {\n    preset: \"ember\" as GshPresetId | undefined,\n    groundParams: undefined as Partial<GradientParams> | undefined,\n    groundOffset: 2.5,\n    panel: \"#0e0e11\",\n    ink: \"244,244,246\",\n    imgEdge: \"rgba(255,255,255,0.1)\",\n    edge: \"0 0 0 1px rgba(255,255,255,0.08), inset 0 1px 0 rgba(255,255,255,0.06)\",\n    shadow: \"0,0,6\",\n    shadowAlpha: 0.6,\n  },\n  light: {\n    preset: \"paper-crease\" as GshPresetId | undefined,\n    groundParams: undefined as Partial<GradientParams> | undefined,\n    groundOffset: 2.5,\n    panel: \"#fbfaf7\",\n    ink: \"22,22,26\",\n    imgEdge: \"rgba(0,0,0,0.1)\",\n    edge: \"0 0 0 1px rgba(20,20,30,0.07)\",\n    shadow: \"40,40,70\",\n    shadowAlpha: 0.3,\n  },\n};\n\nconst DEFAULT_IMAGES = [\n  \"reel/reel-4.jpg\",\n  \"templates/launch-anything/horizon.jpg\",\n  \"templates/product-showcase/gallery.jpg\",\n  \"templates/brand-guidelines/ceramics.jpg\",\n  \"templates/product-showcase/courtyard.jpg\",\n];\n\nconst BEATS = [\n  { name: \"open\", at: 0 },\n  { name: \"dolly\", at: 18 },\n  { name: \"split1\", at: 30 },\n  { name: \"split2\", at: 54 },\n  { name: \"reflow\", at: 72 },\n  { name: \"payoff\", at: 96 },\n  { name: \"end\", at: 144 },\n] as const;\ntype BeatName = (typeof BEATS)[number][\"name\"];\nconst beat = (n: BeatName) => BEATS.find((b) => b.name === n)?.at ?? 0;\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 TYPE_IN = Easing.bezier(0.32, 0.72, 0, 1);\nconst BURST = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ntype Rect = { x: number; y: number; w: number; h: number };\n\nfunction ArrowIcon({ size, color }: { size: number; color: string }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden>\n      <path\n        d=\"M7.5 16.5 L16.5 7.5 M9.5 7.5 H16.5 V14.5\"\n        fill=\"none\"\n        stroke={color}\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction PlayGlyph({ size, color }: { size: number; color: string }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden>\n      <path\n        d=\"M8.5 6.6 L17.5 12 L8.5 17.4 Z\"\n        fill={color}\n        strokeLinejoin=\"round\"\n        stroke={color}\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nfunction Header({\n  left,\n  right,\n  size,\n  pad,\n  w,\n  ink,\n}: {\n  left: ReactNode;\n  right: ReactNode;\n  size: number;\n  pad: number;\n  w?: number;\n  ink: string;\n}) {\n  const fit =\n    w === undefined ? 1 : Math.max(0, Math.min(1, (w - size * 9) / (size * 5)));\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: pad,\n        right: pad,\n        top: pad * 0.8,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"space-between\",\n        fontFamily: mono.fontFamily,\n        fontSize: size,\n        letterSpacing: \"0.04em\",\n        textTransform: \"uppercase\",\n        color: `rgba(${ink},0.5)`,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      <span\n        style={{\n          opacity: Math.max(\n            0,\n            Math.min(1, w === undefined ? 1 : (w - size * 3) / (size * 4)),\n          ),\n        }}\n      >\n        {left}\n      </span>\n      <span style={{ opacity: fit }}>{right}</span>\n    </div>\n  );\n}\n\nexport function PanelSplit({\n  images,\n  name = \"Mara Okafor\",\n  role = \"Motion designer\",\n  headline = [\"Driven by motion,\", \"obsessed with\", \"the seamless.\"],\n  question = \"What if every launch felt this good?\",\n  body = \"A studio practice built on timing, rhythm and the frames in between.\",\n  caption = \"Horizon, brand film\",\n  labels = [\"Portfolio\", \"Case 01\", \"Notes\", \"Reel\"],\n  theme = \"dark\",\n  accent = \"#3B5BFD\",\n  panel,\n  ground,\n  groundParams,\n}: PanelSplitProps) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, fps } = useVideoConfig();\n  const t = THEMES[theme];\n  const portrait = H > W;\n  const srcs =\n    images && images.length > 0 ? images : DEFAULT_IMAGES.map(staticFile);\n  const img = (i: number) => srcs[i % srcs.length] as string;\n  const panelBg = panel ?? t.panel;\n  const shadow = (a: number) => `rgba(${t.shadow},${a * t.shadowAlpha})`;\n  const sp = (at: number, config: Partial<typeof CARD>) =>\n    spring({ frame: frame - at, fps, config });\n\n  const R: Rect = portrait\n    ? { x: W * 0.07, y: H * 0.08, w: W * 0.86, h: H * 0.84 }\n    : { x: W * 0.11, y: H * 0.1, w: W * 0.78, h: H * 0.8 };\n  const u = portrait ? (W / 1080) * 1.45 : W / 1280;\n  const G = portrait ? W * 0.022 : W * 0.011;\n  const radius = portrait ? W * 0.03 : W * 0.014;\n\n  const s1 = sp(beat(\"split1\"), HEAVY);\n  const s2 = sp(beat(\"split2\"), HEAVY);\n  const s2b = sp(beat(\"split2\") + 3, HEAVY);\n  const s3 = sp(beat(\"reflow\"), HEAVY);\n  const F = portrait\n    ? { c1: 0.58, c3: 0.52, l2: 0.66, l3: 0.58, r2: 0.5, r3: 0.42 }\n    : { c1: 0.6, c3: 0.44, l2: 0.64, l3: 0.56, r2: 0.5, r3: 0.62 };\n  const c = 1 - (1 - F.c1) * s1 - (F.c1 - F.c3) * s3;\n  const rL = 1 - (1 - F.l2) * s2 - (F.l2 - F.l3) * s3;\n  const rR = 1 - (1 - F.r2) * s2b - (F.r2 - F.r3) * s3;\n  const gc = G * Math.min(1, s1 * 1.6);\n  const gl = G * Math.min(1, s2 * 1.6);\n  const gr = G * Math.min(1, s2b * 1.6);\n\n  const UL = portrait ? R.h : R.w;\n  const VL = portrait ? R.w : R.h;\n  const toRect = (\n    u0: number,\n    v0: number,\n    u1: number,\n    v1: number,\n    iu0: number,\n    iu1: number,\n    iv0: number,\n    iv1: number,\n  ): Rect => {\n    const a0 = u0 * UL + iu0;\n    const a1 = u1 * UL - iu1;\n    const b0 = v0 * VL + iv0;\n    const b1 = v1 * VL - iv1;\n    return portrait\n      ? { x: R.x + b0, y: R.y + a0, w: b1 - b0, h: a1 - a0 }\n      : { x: R.x + a0, y: R.y + b0, w: a1 - a0, h: b1 - b0 };\n  };\n  const P1 = toRect(0, 0, c, rL, 0, gc / 2, 0, gl / 2);\n  const P2 = toRect(c, 0, 1, rR, gc / 2, 0, 0, gr / 2);\n  const P3 = toRect(0, rL, c, 1, 0, gc / 2, gl / 2, 0);\n  const P4 = toRect(c, rR, 1, 1, gc / 2, 0, gr / 2, 0);\n\n  const openIn = sp(-10, HEAVY);\n  const cam =\n    (0.84 + 0.16 * sp(beat(\"dolly\"), HEAVY)) *\n    interpolate(frame, [0, beat(\"end\")], [1, 1.025]);\n  const payPunch = interpolate(\n    frame,\n    [beat(\"payoff\"), beat(\"payoff\") + 3, beat(\"payoff\") + 12],\n    [0, 1, 0],\n    { ...CLAMP, easing: Easing.bezier(0.65, 0, 0.35, 1) },\n  );\n\n  const label = 16 * u;\n  const pad = 24 * u;\n  const preset = ground ?? (groundParams ? undefined : t.preset);\n  const gParams = groundParams ?? (ground ? undefined : t.groundParams);\n\n  const panelStyle = (r: Rect, extra?: React.CSSProperties) =>\n    ({\n      position: \"absolute\",\n      left: r.x,\n      top: r.y,\n      width: Math.max(0, r.w),\n      height: Math.max(0, r.h),\n      borderRadius: radius,\n      overflow: \"hidden\",\n      backgroundColor: panelBg,\n      boxShadow: `${t.edge}, 0 ${2 * u}px ${6 * u}px ${shadow(0.5)}, 0 ${30 * u}px ${70 * u}px ${shadow(0.55)}`,\n      ...extra,\n    }) as React.CSSProperties;\n\n  const k1 = 0.62 + 0.38 * Math.min(P1.w / R.w, P1.h / R.h);\n  const heroFont = (portrait ? 46 : 44) * u;\n  const lineIn = (i: number) =>\n    interpolate(frame, [-12 + i * 3, 1 + i * 3], [0, 1], {\n      ...CLAMP,\n      easing: TYPE_IN,\n    });\n  const tile = Math.min((P1.h / k1) * 0.4, 200 * u);\n  const tileIn = sp(-4, CARD);\n\n  const p2In = sp(beat(\"split1\") + 6, CARD);\n  const p3In = sp(beat(\"split2\") + 8, CARD);\n  const p4In = (i: number) => sp(beat(\"split2\") + 10 + i * 3, CARD);\n\n  const floodR = interpolate(\n    frame,\n    [beat(\"payoff\"), beat(\"payoff\") + 14],\n    [0, 1],\n    { ...CLAMP, easing: BURST },\n  );\n  const playX = pad;\n  const playY = P4.h - pad - 40 * u;\n  const floodMax = Math.hypot(P4.w, P4.h) * 1.05;\n  const pillPop = sp(beat(\"payoff\") - 2, CHIP);\n\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      <GshGround\n        preset={preset}\n        params={gParams}\n        timeOffset={t.groundOffset}\n        timeScale={0.5}\n      />\n      <AbsoluteFill\n        style={{\n          transform: `scale(${cam}) translateY(${(1 - openIn) * H * 0.06}px)`,\n          opacity: Math.min(1, openIn * 1.6),\n        }}\n      >\n        <div style={panelStyle(P1)}>\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              width: P1.w / k1,\n              height: P1.h / k1,\n              transform: `scale(${k1})`,\n              transformOrigin: \"0 0\",\n              fontFamily: sans.fontFamily,\n              fontWeight: 400,\n              color: `rgb(${t.ink})`,\n            }}\n          >\n            <Header\n              ink={t.ink}\n              size={label}\n              pad={pad}\n              left={<span>{labels[0]}</span>}\n              right={<span>Lisbon</span>}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                left: pad,\n                top: pad * 0.8 + label * 2.4,\n                fontSize: label * 1.5,\n                lineHeight: 1.22,\n                letterSpacing: \"-0.01em\",\n                opacity: lineIn(0),\n                transform: `translateY(${(1 - lineIn(0)) * label}px)`,\n              }}\n            >\n              <div>{name}</div>\n              <div style={{ color: `rgba(${t.ink},0.5)` }}>{role}</div>\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: pad,\n                bottom: pad * 1.1,\n                fontSize: heroFont,\n                lineHeight: 1.06,\n                letterSpacing: \"-0.035em\",\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {headline.map((line, i) => (\n                <div\n                  key={`${line}-${i}`}\n                  style={{\n                    overflow: \"hidden\",\n                    paddingBottom: \"0.08em\",\n                    marginBottom: \"-0.08em\",\n                  }}\n                >\n                  <div\n                    style={{\n                      transform: `translateY(${(1 - lineIn(i + 1)) * 105}%)`,\n                      color: `rgba(${t.ink},${[1, 0.62, 0.42][i] ?? 0.42})`,\n                    }}\n                  >\n                    {line}\n                  </div>\n                </div>\n              ))}\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                right: pad + (P1.w / k1) * 0.16 * (1 - s1),\n                top: pad * 0.8 + label * 2.4,\n                width: tile,\n                height: tile,\n                borderRadius: tile * 0.22,\n                overflow: \"hidden\",\n                transform: `scale(${0.7 + 0.3 * tileIn}) rotate(${(1 - tileIn) * -6}deg)`,\n                opacity: Math.min(1, tileIn * 1.5),\n                boxShadow: `0 ${10 * u}px ${30 * u}px ${shadow(0.6)}`,\n              }}\n            >\n              <Img\n                src={img(0)}\n                style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n              />\n              <div\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  borderRadius: \"inherit\",\n                  boxShadow: `inset 0 0 0 1px ${t.imgEdge}`,\n                }}\n              />\n            </div>\n          </div>\n        </div>\n        {P2.w > 2 && P2.h > 2 ? (\n          <div style={panelStyle(P2)}>\n            <Header\n              ink={t.ink}\n              size={label}\n              pad={pad}\n              w={P2.w}\n              left={<span>{labels[1]}</span>}\n              right={\n                <span style={{ display: \"flex\" }}>\n                  <ArrowIcon size={label * 1.3} color={`rgba(${t.ink},0.7)`} />\n                </span>\n              }\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                left: pad,\n                right: pad,\n                top: pad * 0.8 + label * 2.6,\n                bottom: pad + label * 2.2,\n                borderRadius: Math.max(4, radius - pad * 0.5),\n                overflow: \"hidden\",\n                transform: `translateY(${(1 - p2In) * P2.h * 0.4}px)`,\n                opacity: Math.min(1, p2In * 1.4),\n                outline: `1px solid ${t.imgEdge}`,\n                outlineOffset: -1,\n              }}\n            >\n              <Img\n                src={img(1)}\n                style={{\n                  width: \"100%\",\n                  height: \"100%\",\n                  objectFit: \"cover\",\n                  transform: `scale(${1.18 - 0.12 * p2In - 0.04 * s3})`,\n                }}\n              />\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: pad,\n                right: pad,\n                bottom: pad * 0.9,\n                display: \"flex\",\n                justifyContent: \"space-between\",\n                fontFamily: sans.fontFamily,\n                fontSize: label * 1.08,\n                color: `rgba(${t.ink},0.88)`,\n                whiteSpace: \"nowrap\",\n                opacity: interpolate(p2In, [0.4, 1], [0, 1], CLAMP),\n              }}\n            >\n              <span>{caption}</span>\n              <span\n                style={{\n                  fontFamily: mono.fontFamily,\n                  fontSize: label * 0.95,\n                  fontVariantNumeric: \"tabular-nums\",\n                  color: `rgba(${t.ink},0.45)`,\n                  opacity: interpolate(\n                    P2.w,\n                    [label * 22, label * 26],\n                    [0, 1],\n                    CLAMP,\n                  ),\n                }}\n              >\n                2026\n              </span>\n            </div>\n          </div>\n        ) : null}\n        {P3.w > 2 && P3.h > 2 ? (\n          <div style={panelStyle(P3)}>\n            <Header\n              ink={t.ink}\n              size={label}\n              pad={pad}\n              w={P3.w}\n              left={<span>{labels[2]}</span>}\n              right={<span>02</span>}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                left: pad,\n                right: pad,\n                bottom: pad * 1.1,\n                transform: `translateY(${(1 - p3In) * P3.h * 0.5}px)`,\n                opacity: Math.min(1, p3In * 1.4),\n              }}\n            >\n              <div\n                style={{\n                  fontFamily: serif.fontFamily,\n                  fontStyle: \"italic\",\n                  fontWeight: 400,\n                  fontSize: (portrait ? 33 : 34) * u,\n                  lineHeight: 1.04,\n                  letterSpacing: \"-0.01em\",\n                  color: `rgb(${t.ink})`,\n                  maxWidth: \"12em\",\n                  textWrap: \"balance\",\n                }}\n              >\n                {question}\n              </div>\n              <div\n                style={{\n                  marginTop: label * 0.9,\n                  fontFamily: sans.fontFamily,\n                  fontSize: label * 1.04,\n                  lineHeight: 1.4,\n                  color: `rgba(${t.ink},0.55)`,\n                  maxWidth: \"26em\",\n                  opacity: interpolate(\n                    P3.w,\n                    [label * 22, label * 27],\n                    [0, 1],\n                    CLAMP,\n                  ),\n                }}\n              >\n                {body}\n              </div>\n            </div>\n          </div>\n        ) : null}\n        {P4.w > 2 && P4.h > 2 ? (\n          <div\n            style={panelStyle(P4, {\n              transform: `scale(${1 + payPunch * 0.025})`,\n            })}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                backgroundColor: accent,\n                clipPath: `circle(${floodR * floodMax}px at ${playX + 20 * u}px ${playY + 20 * u}px)`,\n              }}\n            />\n            <Header\n              ink={t.ink}\n              size={label}\n              pad={pad}\n              w={P4.w}\n              left={\n                <span\n                  style={{\n                    color: floodR > 0.5 ? \"rgba(255,255,255,0.8)\" : undefined,\n                  }}\n                >\n                  {labels[3]}\n                </span>\n              }\n              right={\n                <span\n                  style={{\n                    color: floodR > 0.5 ? \"rgba(255,255,255,0.8)\" : undefined,\n                  }}\n                >\n                  0:42\n                </span>\n              }\n            />\n            {[2, 4, 3].map((ii, i) => {\n              const e = p4In(i);\n              const fan = sp(beat(\"payoff\") + 2 + i * 2, CARD);\n              const cw = Math.min(P4.w * 0.36, P4.h * 0.6);\n              const ch = cw * 1.28;\n              const off = [-1, 1, 0][i] as number;\n              const x =\n                P4.w * 0.56 +\n                off * cw * (0.42 + 0.2 * fan) +\n                (1 - e) * P4.w * 0.6;\n              const y = P4.h * 0.5 + (i === 2 ? -ch * 0.04 : ch * 0.04);\n              const rot = off * (5 + 4 * fan) + (1 - e) * 14;\n              return (\n                <div\n                  key={`c-${ii}`}\n                  style={{\n                    position: \"absolute\",\n                    left: x - cw / 2,\n                    top: y - ch / 2,\n                    width: cw,\n                    height: ch,\n                    borderRadius: cw * 0.07,\n                    overflow: \"hidden\",\n                    transform: `rotate(${rot}deg) scale(${i === 2 ? 1.06 + 0.04 * fan : 1})`,\n                    opacity: Math.min(1, e * 1.5),\n                    zIndex: i === 2 ? 2 : 1,\n                    boxShadow: `0 ${4 * u}px ${10 * u}px rgba(0,0,20,0.28), 0 ${22 * u}px ${44 * u}px rgba(0,0,20,0.3)`,\n                  }}\n                >\n                  <Img\n                    src={img(ii)}\n                    style={{\n                      width: \"100%\",\n                      height: \"100%\",\n                      objectFit: \"cover\",\n                    }}\n                  />\n                </div>\n              );\n            })}\n            <div\n              style={{\n                position: \"absolute\",\n                left: playX,\n                top: playY,\n                height: 40 * u,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 10 * u,\n                padding: `0 ${16 * u}px 0 ${4 * u}px`,\n                borderRadius: 40 * u,\n                backgroundColor:\n                  floodR > 0.05\n                    ? \"rgba(255,255,255,0.96)\"\n                    : `rgba(${t.ink},0.08)`,\n                boxShadow:\n                  floodR > 0.05\n                    ? \"0 6px 18px rgba(0,0,40,0.3)\"\n                    : `0 0 0 1px rgba(${t.ink},0.14)`,\n                fontFamily: sans.fontFamily,\n                fontSize: label * 1.04,\n                color: floodR > 0.05 ? \"#0c0c10\" : `rgba(${t.ink},0.9)`,\n                transform: `scale(${0.9 + 0.1 * pillPop})`,\n                transformOrigin: \"20% 50%\",\n                opacity: Math.min(1, p4In(0) * 1.4),\n                zIndex: 3,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              <span\n                style={{\n                  width: 32 * u,\n                  height: 32 * u,\n                  borderRadius: \"50%\",\n                  backgroundColor:\n                    floodR > 0.05 ? accent : `rgba(${t.ink},0.5)`,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                }}\n              >\n                <PlayGlyph size={16 * u} color=\"#ffffff\" />\n              </span>\n              Play reel\n            </div>\n          </div>\n        ) : null}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n\nexport default PanelSplit;\n",
      "type": "registry:component",
      "target": "components/remocn/panel-split.tsx"
    }
  ],
  "type": "registry:component"
}
