{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "before-after",
  "title": "Before After",
  "description": "A divider sweeps across a photo to reveal the graded version of an ungraded still.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/before-after/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadHanken } from \"@remotion/google-fonts/HankenGrotesk\";\nimport type React 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 hanken = loadHanken(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface BeforeAfterProps {\n  before?: string;\n  after?: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  title?: string;\n  aspect?: number;\n  fit?: \"cover\" | \"contain\";\n  pauseAt?: number;\n  accent?: string;\n  background?: string;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    preset: \"hearth\" as GshPresetId,\n    scrim:\n      \"linear-gradient(180deg, rgba(5,3,2,0.72) 0%, rgba(5,3,2,0.2) 34%, rgba(5,3,2,0) 60%)\",\n    fg: \"#F6F1EA\",\n    dim: \"rgba(246,241,234,0.56)\",\n    outline: \"rgba(255,255,255,0.10)\",\n    shadow:\n      \"0 1px 0 rgba(255,255,255,0.06) inset, 0 2px 4px rgba(0,0,0,0.3), 0 18px 40px -12px rgba(0,0,0,0.6), 0 50px 110px -40px rgba(0,0,0,0.8)\",\n  },\n  light: {\n    preset: \"paper-crease\" as GshPresetId,\n    scrim:\n      \"linear-gradient(180deg, rgba(238,236,232,0.7) 0%, rgba(238,236,232,0.15) 36%, rgba(238,236,232,0) 60%)\",\n    fg: \"#16151A\",\n    dim: \"rgba(22,21,26,0.52)\",\n    outline: \"rgba(0,0,0,0.10)\",\n    shadow:\n      \"0 1px 2px rgba(26,31,74,0.08), 0 14px 32px -12px rgba(26,31,74,0.22), 0 44px 90px -40px rgba(26,31,74,0.35)\",\n  },\n} as const;\n\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst EXIT = Easing.bezier(0.55, 0, 1, 0.45);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\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 };\n\nconst BEAT = {\n  frame: 0,\n  title: 4,\n  grip: 18,\n  leg1: 24,\n  leg2: 78,\n  gripOut: 104,\n};\n\nconst DEFAULT_SRC = \"bg.jpg\";\nconst FLAT = \"grayscale(0.85) contrast(0.78) brightness(0.9)\";\nconst GRADED = \"saturate(1.18) contrast(1.06)\";\n\nfunction Chevrons({ size, color }: { size: number; color: string }) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      <path d=\"M9.5 7.5 5 12l4.5 4.5\" />\n      <path d=\"M14.5 7.5 19 12l-4.5 4.5\" />\n    </svg>\n  );\n}\n\nexport function BeforeAfter({\n  before,\n  after,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  title,\n  aspect,\n  fit = \"cover\",\n  pauseAt = 0.5,\n  accent = \"#3B5BFD\",\n  background,\n  ground,\n  groundParams,\n  speed = 1,\n  theme = \"dark\",\n}: BeforeAfterProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const p = THEMES[theme];\n\n  const portrait = height > width * 1.1;\n  const s = portrait\n    ? (width / 1280) * 1.5\n    : Math.min(width / 1280, height / 720);\n  const ratio = aspect ?? (portrait ? 4 / 5 : 16 / 9);\n\n  const titleFs = (portrait ? 66 : 44) * s;\n  const capFs = (portrait ? 24 : 19) * s;\n  const titleGap = (portrait ? 36 : 22) * s;\n  const capGap = (portrait ? 26 : 18) * s;\n  const titleH = title ? titleFs * 1.1 + titleGap : 0;\n  const capH = capFs * 1.2 + capGap;\n  const maxW = width * (portrait ? 0.8 : 0.7);\n  const maxH = height * (portrait ? 0.62 : 0.86) - titleH - capH;\n  const frameH = Math.min(maxH, maxW / ratio);\n  const frameW = frameH * ratio;\n  const groupH = titleH + frameH + capH;\n  const groupTop = (height - groupH) / 2;\n  const frameTop = groupTop + titleH;\n  const frameLeft = (width - frameW) / 2;\n  const radius = 16 * s;\n\n  const at = (start: number, config: Record<string, number>) =>\n    spring({ frame: frame - start, fps, config });\n\n  const enter = at(BEAT.frame, HEAVY);\n  const fadeIn = interpolate(frame, [0, 10], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const titleT = interpolate(frame, [BEAT.title, BEAT.title + 16], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const leg1 = at(BEAT.leg1, CARD);\n  const leg2 = at(BEAT.leg2, MORPH);\n  const pos =\n    frame >= BEAT.leg2 ? pauseAt * (1 - leg2) : 1 - (1 - pauseAt) * leg1;\n  const push = interpolate(frame, [0, 150], [1, 1.02], CLAMP);\n\n  const gripIn = at(BEAT.grip, CHIP);\n  const gripOut = interpolate(frame, [BEAT.gripOut, BEAT.gripOut + 8], [0, 1], {\n    ...CLAMP,\n    easing: EXIT,\n  });\n  const pressed =\n    1 - 0.04 * (at(BEAT.leg1 - 5, CHIP) - at(BEAT.leg1 + 1, CHIP));\n  const grip = Math.min(1, gripIn) * (1 - gripOut);\n\n  const divX = Math.max(0, Math.min(1, pos)) * frameW;\n  const capsIn = interpolate(frame, [BEAT.leg1, BEAT.leg1 + 12], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n\n  const beforeSrc = before ?? staticFile(DEFAULT_SRC);\n  const afterSrc = after ?? staticFile(DEFAULT_SRC);\n  const imgStyle = (\n    filter: string | undefined,\n    builtIn: boolean,\n  ): React.CSSProperties => ({\n    position: \"absolute\",\n    inset: 0,\n    width: \"100%\",\n    height: \"100%\",\n    objectFit: fit,\n    filter,\n    ...(builtIn\n      ? { transform: \"scale(1.45)\", transformOrigin: \"16% 38%\" }\n      : {}),\n  });\n\n  const transparent = background === \"transparent\";\n  const gripD = (portrait ? 64 : 44) * s;\n  const label: React.CSSProperties = {\n    position: \"absolute\",\n    top: frameTop + frameH + capGap,\n    fontFamily: hanken.fontFamily,\n    fontWeight: 400,\n    fontSize: capFs,\n    lineHeight: 1.2,\n    letterSpacing: \"-0.005em\",\n    whiteSpace: \"nowrap\",\n    opacity: capsIn,\n  };\n  const tick = (color: string): React.CSSProperties => ({\n    display: \"inline-block\",\n    width: capFs * 0.42,\n    height: capFs * 0.42,\n    borderRadius: capFs,\n    background: color,\n    verticalAlign: \"0.08em\",\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: transparent ? \"transparent\" : (background ?? \"#050302\"),\n        overflow: \"hidden\",\n        fontFamily: hanken.fontFamily,\n      }}\n    >\n      {!transparent && background === undefined ? (\n        <AbsoluteFill style={{ opacity: fadeIn }}>\n          <GshGround\n            preset={ground ?? p.preset}\n            params={groundParams}\n            timeOffset={3}\n            timeScale={0.5}\n          />\n          <AbsoluteFill style={{ background: p.scrim }} />\n        </AbsoluteFill>\n      ) : null}\n\n      {title ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: frameLeft,\n            top: groupTop,\n            width: frameW,\n            height: titleFs * 1.2,\n            overflow: \"hidden\",\n          }}\n        >\n          <div\n            style={{\n              fontWeight: 300,\n              fontSize: titleFs,\n              lineHeight: 1.1,\n              letterSpacing: \"-0.03em\",\n              color: p.fg,\n              whiteSpace: \"nowrap\",\n              transform: `translateY(${(1 - titleT) * 105}%)`,\n            }}\n          >\n            {title}\n          </div>\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: frameLeft,\n          top: frameTop,\n          width: frameW,\n          height: frameH,\n          borderRadius: radius,\n          overflow: \"hidden\",\n          boxShadow: p.shadow,\n          opacity: fadeIn,\n          transform: `translateY(${(1 - enter) * 40 * s}px) scale(${(0.96 + enter * 0.04) * push})`,\n          background: \"#111\",\n        }}\n      >\n        <Img\n          src={afterSrc}\n          style={imgStyle(after ? undefined : GRADED, !after)}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            clipPath: `inset(0 ${(100 - (divX / frameW) * 100).toFixed(4)}% 0 0)`,\n          }}\n        >\n          <Img\n            src={beforeSrc}\n            style={imgStyle(before ? undefined : FLAT, !before)}\n          />\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            top: 0,\n            bottom: 0,\n            left: divX - 1 * s,\n            width: 2 * s,\n            background: \"rgba(255,255,255,0.95)\",\n            boxShadow:\n              \"0 0 0 0.5px rgba(0,0,0,0.12), 0 0 12px rgba(0,0,0,0.25)\",\n            opacity: grip,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: divX - gripD / 2,\n            top: frameH / 2 - gripD / 2,\n            width: gripD,\n            height: gripD,\n            borderRadius: gripD,\n            background: \"#FFFFFF\",\n            boxShadow:\n              \"0 1px 2px rgba(0,0,0,0.25), 0 8px 20px -6px rgba(0,0,0,0.45)\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            opacity: grip,\n            transform: `scale(${(0.6 + 0.4 * gripIn) * pressed})`,\n          }}\n        >\n          <Chevrons size={gripD * 0.56} color=\"#2A2A30\" />\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: radius,\n            boxShadow: `inset 0 0 0 1px ${p.outline}`,\n            pointerEvents: \"none\",\n          }}\n        />\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: frameLeft,\n          top: 0,\n          width: frameW,\n          height,\n          overflow: \"hidden\",\n          pointerEvents: \"none\",\n        }}\n      >\n        <div\n          style={{\n            ...label,\n            right: frameW - divX + capGap * 0.9,\n            color: p.dim,\n          }}\n        >\n          {beforeLabel} <span style={tick(p.dim)} />\n        </div>\n        <div style={{ ...label, left: divX + capGap * 0.9, color: p.fg }}>\n          <span style={tick(accent)} /> {afterLabel}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/before-after.tsx"
    }
  ],
  "type": "registry:component"
}
