{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "up-close",
  "title": "Up Close",
  "description": "A screenshot zooms close, captioned \"Up close, it's code. From here, it's your launch.\".",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/up-close/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont } from \"@remotion/google-fonts/Onest\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\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 onest = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst mono = loadMono(\"normal\", { weights: [\"400\"], subsets: [\"latin\"] });\n\nexport interface UpCloseProps {\n  close?: string;\n  wide?: string;\n  src?: string;\n  aspect?: number;\n  focus?: { x: number; y: number };\n  zoom?: 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: \"film-blue\" as GshPresetId,\n    bg: \"#03070f\",\n    ink: \"#F3F6FA\",\n    dim: \"rgba(243,246,250,0.42)\",\n    faint: \"rgba(243,246,250,0.22)\",\n    accent: \"#9CC8FF\",\n    well: \"#0B1320\",\n    scrim: \"linear-gradient(180deg, rgba(3,7,15,0) 40%, rgba(3,7,15,0.7) 100%)\",\n    shadow:\n      \"inset 0 1px 0 rgba(255,255,255,0.07), 0 0 0 1px rgba(255,255,255,0.09), 0 2px 6px rgba(0,0,0,0.35), 0 24px 60px -18px rgba(0,0,0,0.7), 0 60px 140px -50px rgba(0,0,0,0.85)\",\n  },\n  light: {\n    preset: \"paper-crease\" as GshPresetId,\n    bg: \"#eeece8\",\n    ink: \"#12131A\",\n    dim: \"rgba(18,19,26,0.42)\",\n    faint: \"rgba(18,19,26,0.2)\",\n    accent: \"#2742C9\",\n    well: \"#D9DCE8\",\n    scrim:\n      \"linear-gradient(180deg, rgba(238,236,232,0) 40%, rgba(238,236,232,0.7) 100%)\",\n    shadow:\n      \"0 0 0 1px rgba(26,31,74,0.08), 0 2px 4px rgba(26,31,74,0.06), 0 22px 50px -18px rgba(26,31,74,0.24), 0 60px 120px -50px rgba(26,31,74,0.32)\",\n  },\n} as const;\n\nconst WGHT = 400;\nconst TRACK = -0.03;\nconst PX = 200;\nconst LEADING = 1.12;\n\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst MOVE = Easing.bezier(0.65, 0, 0.35, 1);\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ntype Widths = (s: string) => number;\n\nfunction buildWidths(_version: number): Widths {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n  const cache: Record<string, number> = {};\n  return (s: string) => {\n    if (cache[s] === undefined) {\n      if (!ctx) return s.length * 0.52;\n      ctx.font = `${WGHT} ${PX}px \"${onest.fontFamily}\"`;\n      cache[s] = ctx.measureText(s).width / PX + TRACK * Array.from(s).length;\n    }\n    return cache[s];\n  };\n}\n\nfunction useWidths() {\n  const [version, setVersion] = useState(0);\n  const [handle] = useState(() => delayRender(\"up-close: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const bump = () => {\n      if (live) setVersion((v) => v + 1);\n    };\n    document.fonts.addEventListener(\"loadingdone\", bump);\n    Promise.all([\n      onest.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${onest.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        bump();\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n      document.fonts.removeEventListener(\"loadingdone\", bump);\n    };\n  }, [handle]);\n  return useMemo(() => buildWidths(version), [version]);\n}\n\nfunction wrap(\n  words: string[],\n  w: Widths,\n  sp: number,\n  size: number,\n  maxW: number,\n) {\n  const lines: string[][] = [[]];\n  let run = 0;\n  for (const wd of words) {\n    const line = lines[lines.length - 1];\n    const add = (line.length > 0 ? sp : 0) + w(wd);\n    if (line.length > 0 && (run + add) * size > maxW) {\n      lines.push([wd]);\n      run = w(wd);\n    } else {\n      line.push(wd);\n      run += add;\n    }\n  }\n  return lines.map((l) => l.join(\" \"));\n}\n\nfunction ZoomReadout({\n  value,\n  progress,\n  size,\n  ink,\n  dim,\n  faint,\n}: {\n  value: number;\n  progress: number;\n  size: number;\n  ink: string;\n  dim: string;\n  faint: string;\n}) {\n  const ticks = 25;\n  const rulerW = size * 4;\n  return (\n    <div style={{ display: \"flex\", flexDirection: \"column\", gap: size * 0.3 }}>\n      <div\n        style={{\n          fontFamily: mono.fontFamily,\n          fontWeight: 400,\n          fontSize: size * 0.36,\n          letterSpacing: \"0.04em\",\n          color: dim,\n          textTransform: \"uppercase\",\n        }}\n      >\n        Zoom\n      </div>\n      <div\n        style={{\n          fontFamily: onest.fontFamily,\n          fontWeight: 300,\n          fontSize: size,\n          lineHeight: 1,\n          letterSpacing: \"-0.03em\",\n          color: ink,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        {value.toFixed(1)}×\n      </div>\n      <div\n        style={{\n          position: \"relative\",\n          width: rulerW,\n          height: size * 0.34,\n          marginTop: size * 0.1,\n        }}\n      >\n        {Array.from({ length: ticks }, (_, i) => (\n          <span\n            key={i.toString()}\n            style={{\n              position: \"absolute\",\n              left: (i / (ticks - 1)) * rulerW,\n              bottom: 0,\n              width: 1,\n              height: i % 6 === 0 ? size * 0.26 : size * 0.14,\n              background: faint,\n            }}\n          />\n        ))}\n        <span\n          style={{\n            position: \"absolute\",\n            left: (1 - progress) * rulerW - 1,\n            bottom: 0,\n            width: 2,\n            height: size * 0.34,\n            borderRadius: 1,\n            background: ink,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport function UpClose({\n  close = \"Up close, it’s code.\",\n  wide = \"From here, it’s your launch.\",\n  src,\n  aspect = 1200 / 630,\n  focus = { x: 0.58, y: 0.52 },\n  zoom = 3,\n  accent,\n  background,\n  ground,\n  groundParams,\n  speed = 1,\n  theme = \"dark\",\n}: UpCloseProps) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, durationInFrames: D, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const hi = accent ?? c.accent;\n  const transparent = background === \"transparent\";\n  const image = src ?? staticFile(\"hero.png\");\n  const widths = useWidths();\n\n  const closeWords = close.split(/\\s+/).filter(Boolean);\n  const wideWords = wide.split(/\\s+/).filter(Boolean);\n\n  const k = Math.min(1.6, Math.max(0.6, D / 125));\n  const t = frame * speed;\n  const portrait = H > W * 1.1;\n  const sp = widths(\" \");\n  const lerp = (a: number, b: number, q: number) => a + (b - a) * q;\n\n  const textX = W * (portrait ? 0.08 : 0.065);\n  const maxW = W * (portrait ? 0.84 : 0.86);\n  let size = portrait ? W * 0.078 : H * 0.072;\n  let closeLines = wrap(closeWords, widths, sp, size, maxW);\n  let wideLines = wrap(wideWords, widths, sp, size, maxW);\n  for (let guard = 0; guard < 10; guard++) {\n    if (closeLines.length + wideLines.length <= (portrait ? 5 : 3)) break;\n    size *= 0.92;\n    closeLines = wrap(closeWords, widths, sp, size, maxW);\n    wideLines = wrap(wideWords, widths, sp, size, maxW);\n  }\n  const step = LEADING * size;\n  const bottom = H * (portrait ? 0.92 : 0.9);\n  const wideTop = bottom - size - (wideLines.length - 1) * step;\n  const closeFinalTop = wideTop - closeLines.length * step;\n  const closeStartTop = bottom - size - (closeLines.length - 1) * step;\n\n  const margin = W * (portrait ? 0.11 : 0.065);\n  const finalW = portrait ? W - margin * 2 : W * 0.6;\n  const finalAspect = portrait ? Math.min(aspect, 0.82) : aspect;\n  const finalTopY = H * (portrait ? 0.2 : 0.08);\n  const finalH = Math.min(\n    finalW / finalAspect,\n    closeFinalTop - size * 0.6 - finalTopY,\n  );\n  const finalWFit = finalH * finalAspect;\n  const win0 = {\n    x: 0,\n    y: 0,\n    w: W,\n    h: closeStartTop - size * 0.62,\n  };\n  const win1 = {\n    x: portrait ? (W - finalWFit) / 2 : W - margin - finalWFit,\n    y: finalTopY,\n    w: finalWFit,\n    h: finalH,\n  };\n\n  const PULL_AT = 44 * k;\n  const WIDE_AT = PULL_AT + 20 * k;\n\n  const q = Math.min(1, spring({ frame: t - PULL_AT, fps, config: HEAVY }));\n  const win = {\n    x: lerp(win0.x, win1.x, q),\n    y: lerp(win0.y, win1.y, q),\n    w: lerp(win0.w, win1.w, q),\n    h: lerp(win0.h, win1.h, q),\n  };\n  const radius = lerp(0, Math.min(W, H) * 0.018, q);\n\n  const creep = interpolate(t, [0, PULL_AT], [1.05, 1], CLAMP);\n  const settle = interpolate(t, [PULL_AT, D], [1.03, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  const z = zoom ** (1 - q) * lerp(creep, settle, q);\n  const coverW = Math.max(win.w, win.h * aspect);\n  const coverH = coverW / aspect;\n  const imgW = coverW * z;\n  const imgH = coverH * z;\n  const ax = lerp(focus.x, 0.5, q);\n  const ay = lerp(focus.y, 0.5, q);\n  const left = Math.min(0, Math.max(win.w - imgW, win.w / 2 - ax * imgW));\n  const top = Math.min(0, Math.max(win.h - imgH, win.h / 2 - ay * imgH));\n\n  const fadeIn = interpolate(t, [0, 8 * k], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const closeIn = interpolate(t, [6 * k, 20 * k], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const lift = interpolate(t, [WIDE_AT - 6 * k, WIDE_AT + 12 * k], [0, 1], {\n    ...CLAMP,\n    easing: MOVE,\n  });\n  const wideIn = interpolate(t, [WIDE_AT, WIDE_AT + 14 * k], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const readoutIn = interpolate(t, [PULL_AT, PULL_AT + 10 * k], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const closeTop = lerp(closeStartTop, closeFinalTop, lift);\n  const readSize = portrait ? W * 0.075 : H * 0.085;\n\n  const text = {\n    position: \"absolute\" as const,\n    left: textX,\n    fontFamily: onest.fontFamily,\n    fontWeight: WGHT,\n    fontSize: size,\n    lineHeight: 1,\n    letterSpacing: `${TRACK}em`,\n    whiteSpace: \"pre\" as const,\n  };\n  const rise = (p: number) => (1 - p) * size * 0.32;\n  const blur = (p: number) =>\n    p < 1 ? `blur(${(1 - p) * size * 0.08}px)` : undefined;\n\n  const [lastWide, ...restRev] = [...wideLines].reverse();\n  const wideHead = restRev.reverse();\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: transparent ? \"transparent\" : (background ?? c.bg),\n        overflow: \"hidden\",\n      }}\n    >\n      {!transparent && background === undefined ? (\n        <AbsoluteFill>\n          <GshGround\n            preset={ground ?? c.preset}\n            params={groundParams}\n            timeOffset={1.5}\n            timeScale={0.4}\n          />\n          <AbsoluteFill style={{ background: c.scrim }} />\n        </AbsoluteFill>\n      ) : null}\n      <div\n        style={{\n          position: \"absolute\",\n          left: win.x,\n          top: win.y,\n          width: win.w,\n          height: win.h,\n          borderRadius: radius,\n          overflow: \"hidden\",\n          background: c.well,\n          boxShadow: q > 0.01 ? c.shadow : \"none\",\n          opacity: fadeIn,\n        }}\n      >\n        <Img\n          src={image}\n          style={{\n            position: \"absolute\",\n            left,\n            top,\n            width: imgW,\n            height: imgH,\n            maxWidth: \"none\",\n          }}\n        />\n      </div>\n      <div\n        style={{\n          position: \"absolute\",\n          left: textX,\n          top: portrait ? H * 0.06 : finalTopY,\n          opacity: readoutIn,\n          transform: `translateY(${(1 - readoutIn) * readSize * 0.3}px)`,\n        }}\n      >\n        <ZoomReadout\n          value={1 + (zoom - 1) * (1 - q)}\n          progress={q}\n          size={readSize}\n          ink={c.ink}\n          dim={c.dim}\n          faint={c.faint}\n        />\n      </div>\n      {closeLines.map((ln, i) => (\n        <div\n          key={`c${i.toString()}`}\n          style={{\n            ...text,\n            top: closeTop + i * step + rise(closeIn),\n            opacity: closeIn,\n            filter: blur(closeIn),\n            color: lift > 0 ? mix(c.ink, c.dim, lift) : c.ink,\n          }}\n        >\n          {ln}\n        </div>\n      ))}\n      {wideHead.map((ln, i) => (\n        <div\n          key={`w${i.toString()}`}\n          style={{\n            ...text,\n            top: wideTop + i * step + rise(wideIn),\n            opacity: wideIn,\n            filter: blur(wideIn),\n            color: c.ink,\n          }}\n        >\n          {ln}\n        </div>\n      ))}\n      {lastWide !== undefined ? (\n        <div\n          style={{\n            ...text,\n            top: wideTop + wideHead.length * step + rise(wideIn),\n            opacity: wideIn,\n            filter: blur(wideIn),\n            color: c.ink,\n          }}\n        >\n          <Accented line={lastWide} color={hi} />\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n\nfunction Accented({ line, color }: { line: string; color: string }) {\n  const m = line.match(/^(.*?)([\\p{L}\\p{N}'’-]+)([^\\p{L}\\p{N}]*)$/u);\n  if (!m) return <>{line}</>;\n  return (\n    <>\n      {m[1]}\n      <span style={{ color }}>{m[2]}</span>\n      {m[3]}\n    </>\n  );\n}\n\nfunction mix(a: string, b: string, q: number) {\n  return `color-mix(in oklab, ${a} ${Math.round((1 - q) * 100)}%, ${b})`;\n}\n",
      "type": "registry:component",
      "target": "components/remocn/up-close.tsx"
    }
  ],
  "type": "registry:component"
}
