{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plus-open",
  "title": "Plus Open",
  "description": "A plus sign opens into the next statement on a soft aurora ground, 'Here's everything new.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/plus-open/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSora } from \"@remotion/google-fonts/Sora\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst sora = loadSora(\"normal\", {\n  weights: [\"300\", \"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PlusOpenProps {\n  value?: string;\n  label?: string;\n  next?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    ink: \"#F4F3F0\",\n    muted: \"#8E8D94\",\n    nextInk: \"#F7F6F2\",\n    fieldBg: \"#03050b\",\n    blobs: (a: string) => [\"#a0f0c8\", \"#3fb5a3\", a, \"#b25c9c\"],\n    blobMix: 88,\n    band: \"#a0f0c8\",\n    boost: 1.1,\n    grain: 0.05,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    ink: \"#111113\",\n    muted: \"#6E6C67\",\n    nextInk: \"#111113\",\n    fieldBg: \"#f4f3f1\",\n    blobs: (a: string) => [\"#7a3cff\", \"#ff4fb8\", a, \"#8f6bff\"],\n    blobMix: 70,\n    band: \"#ff4fb8\",\n    boost: 0,\n    grain: 0.035,\n  },\n} as const;\n\nconst TRACK = -0.035;\nconst FONT_PX = 400;\n\nconst RISE_AT = 0;\nconst RISE_LEN = 18;\nconst RISE_GAP = 3;\nconst PLUS_AT = 12;\nconst LABEL_AT = 16;\nconst ARMS_AT = 56;\nconst ARMS_LEN = 14;\nconst OPEN_AT = 70;\nconst OPEN_LEN = 28;\nconst NEXT_AT = 82;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OPEN = Easing.bezier(0.7, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Metrics {\n  x: number[];\n  width: number;\n  cap: number;\n  base: number;\n  label: number;\n}\n\nfunction measure(chars: string[], label: string): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"plus-open: no 2d context\");\n  ctx.font = `400 ${FONT_PX}px \"${sora.fontFamily}\"`;\n  const x: number[] = [];\n  let prefix = \"\";\n  for (let i = 0; i < chars.length; i++) {\n    x.push(ctx.measureText(prefix).width / FONT_PX + TRACK * i);\n    prefix += chars[i];\n  }\n  const width =\n    ctx.measureText(prefix).width / FONT_PX +\n    TRACK * Math.max(0, chars.length - 1);\n  const zm = ctx.measureText(\"0\");\n  const asc = zm.fontBoundingBoxAscent / FONT_PX;\n  const desc = zm.fontBoundingBoxDescent / FONT_PX;\n  ctx.font = `300 ${FONT_PX}px \"${sora.fontFamily}\"`;\n  return {\n    x,\n    width,\n    cap: zm.actualBoundingBoxAscent / FONT_PX,\n    base: (1 - (asc + desc)) / 2 + asc,\n    label: label ? ctx.measureText(label).width / FONT_PX : 0,\n  };\n}\n\nfunction useMetrics(chars: string[], label: string) {\n  const [metrics, setMetrics] = useState<Metrics | null>(null);\n  const [handle] = useState(() => delayRender(\"plus-open: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      sora.waitUntilDone(),\n      document.fonts.load(`400 100px \"${sora.fontFamily}\"`),\n      document.fonts.load(`300 100px \"${sora.fontFamily}\"`),\n      document.fonts.load(`500 100px \"${sora.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setMetrics(measure(chars, label));\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [chars, label, handle]);\n  return metrics;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `po${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        opacity,\n        mixBlendMode: \"overlay\",\n        pointerEvents: \"none\",\n      }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={9}\n        />\n        <feColorMatrix type=\"saturate\" values=\"0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nfunction crossPolygon(\n  cx: number,\n  cy: number,\n  h: number,\n  arms: { l: number; r: number; u: number; d: number },\n  angle: number,\n) {\n  const { l, r, u, d } = arms;\n  const pts: [number, number][] = [\n    [-h, -u],\n    [h, -u],\n    [h, -h],\n    [r, -h],\n    [r, h],\n    [h, h],\n    [h, d],\n    [-h, d],\n    [-h, h],\n    [-l, h],\n    [-l, -h],\n    [-h, -h],\n  ];\n  const cs = Math.cos(angle);\n  const sn = Math.sin(angle);\n  return `polygon(${pts\n    .map(\n      ([x, y]) =>\n        `${(cx + x * cs - y * sn).toFixed(2)}px ${(cy + x * sn + y * cs).toFixed(2)}px`,\n    )\n    .join(\", \")})`;\n}\n\nexport function PlusOpen({\n  value = \"150\",\n  label = \"updates in this release\",\n  next = \"Here’s everything new.\",\n  accent = \"#3B5BFD\",\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: PlusOpenProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const bg = background ?? c.bg;\n\n  const chars = useMemo(() => Array.from(value), [value]);\n  const m = useMetrics(chars, label);\n\n  if (!m) {\n    return <div style={{ position: \"absolute\", inset: 0, background: bg }} />;\n  }\n\n  const plusSpan = m.cap * 0.74;\n  const stroke = 0.082;\n  const plusGap = 0.07;\n  const lockW = m.width + plusGap + plusSpan;\n  const F = Math.min(H * 0.4, (W * 0.74) / lockW);\n\n  const labelSize = label\n    ? Math.min(Math.max(F * 0.13, H * 0.04), H * 0.052, (W * 0.8) / m.label)\n    : 0;\n  const gap = label ? Math.max(F * 0.26, H * 0.05) : 0;\n  const blockH = m.cap * F + gap + labelSize;\n  const baseY = (H - blockH) / 2 + m.cap * F;\n  const left = (W - lockW * F) / 2;\n\n  const cx = left + (m.width + plusGap + plusSpan / 2) * F;\n  const cy = baseY - (m.cap * F) / 2;\n  const h0 = (stroke * F) / 2;\n  const arm0 = (plusSpan * F) / 2;\n\n  const land = spring({\n    frame: t - PLUS_AT,\n    fps,\n    config: { damping: 15, stiffness: 120, mass: 0.9 },\n  });\n  const plusScale = 0.55 + 0.45 * land;\n  const plusAngle = (-Math.PI / 2) * (1 - land);\n  const plusOn = interpolate(t, [PLUS_AT, PLUS_AT + 5], [0, 1], CLAMP);\n\n  const armH = interpolate(t, [ARMS_AT, ARMS_AT + ARMS_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const armV = interpolate(t, [ARMS_AT + 3, ARMS_AT + 3 + ARMS_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const arms = {\n    l: arm0 + (cx + 4 - arm0) * armH,\n    r: arm0 + (W - cx + 4 - arm0) * armH,\n    u: arm0 + (cy + 4 - arm0) * armV,\n    d: arm0 + (H - cy + 4 - arm0) * armV,\n  };\n  const open = interpolate(t, [OPEN_AT, OPEN_AT + OPEN_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OPEN,\n  });\n  const spread = open * Math.max(cx, W - cx, cy, H - cy) * 1.04;\n  const split = t >= OPEN_AT;\n\n  const drift = t / 30;\n  const blobs = c.blobs(accent);\n  const field = [\n    ...blobs.map((col, i) => {\n      const bx =\n        15 + (70 * i) / (blobs.length - 1) + Math.sin(drift * 0.6 + i) * 5;\n      const by = 34 + Math.cos(drift * 0.5 + i * 1.7) * 9 + (i % 2) * 22;\n      return `radial-gradient(38% 62% at ${bx.toFixed(2)}% ${by.toFixed(2)}%, color-mix(in oklab, ${col} ${c.blobMix}%, transparent) 0%, transparent 70%)`;\n    }),\n    `linear-gradient(180deg, transparent 30%, color-mix(in oklab, ${c.band} 34%, transparent) 62%, transparent 92%)`,\n    c.fieldBg,\n  ].join(\", \");\n\n  const rise = (i: number) =>\n    interpolate(\n      t,\n      [RISE_AT + i * RISE_GAP, RISE_AT + i * RISE_GAP + RISE_LEN],\n      [0, 1],\n      { ...CLAMP, easing: EASE_OUT },\n    );\n  const maskTop = baseY - m.cap * F - F * 0.1;\n  const maskH = m.cap * F + F * 0.32;\n  const labelP = interpolate(t, [LABEL_AT, LABEL_AT + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const oldScene = (\n    <>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: bg,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          width: W,\n          top: maskTop,\n          height: maskH,\n          overflow: \"hidden\",\n        }}\n      >\n        {chars.map((ch, i) => (\n          <div\n            key={`${ch}-${i}`}\n            style={{\n              position: \"absolute\",\n              left: left + m.x[i] * F,\n              top: baseY - maskTop - m.base * F + (1 - rise(i)) * maskH * 1.05,\n              fontFamily: sora.fontFamily,\n              fontWeight: 400,\n              fontSize: F,\n              lineHeight: `${F}px`,\n              color: c.ink,\n              whiteSpace: \"pre\",\n              fontFeatureSettings: '\"lnum\" 1',\n            }}\n          >\n            {ch}\n          </div>\n        ))}\n      </div>\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            width: W,\n            top: baseY + gap - labelSize * 0.25,\n            height: labelSize * 1.5,\n            overflow: \"hidden\",\n            textAlign: \"center\",\n            fontFamily: sora.fontFamily,\n            fontWeight: 300,\n            fontSize: labelSize,\n            lineHeight: `${labelSize * 1.3}px`,\n            letterSpacing: \"-0.005em\",\n            color: c.muted,\n            whiteSpace: \"pre\",\n          }}\n        >\n          <div\n            style={{\n              transform: `translateY(${((1 - labelP) * labelSize * 1.4).toFixed(2)}px)`,\n            }}\n          >\n            {label}\n          </div>\n        </div>\n      ) : null}\n    </>\n  );\n\n  const quarters = [\n    { key: \"tl\", sx: -1, sy: -1 },\n    { key: \"tr\", sx: 1, sy: -1 },\n    { key: \"bl\", sx: -1, sy: 1 },\n    { key: \"br\", sx: 1, sy: 1 },\n  ].map(({ key, sx, sy }) => {\n    const top = sy < 0 ? 0 : cy + h0;\n    const bottom = sy < 0 ? H - (cy - h0) : 0;\n    const l = sx < 0 ? 0 : cx + h0;\n    const r = sx < 0 ? W - (cx - h0) : 0;\n    return (\n      <div\n        key={key}\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          clipPath: `inset(${top.toFixed(2)}px ${r.toFixed(2)}px ${bottom.toFixed(2)}px ${l.toFixed(2)}px)`,\n          transform: `translate(${(sx * spread).toFixed(2)}px, ${(sy * spread).toFixed(2)}px)`,\n        }}\n      >\n        {oldScene}\n      </div>\n    );\n  });\n\n  const nextWords = next.split(\" \");\n  const nextSize = Math.min(H * 0.13, W * 0.1);\n\n  const nextScene = (\n    <>\n      <div style={{ position: \"absolute\", inset: 0, background: field }} />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          padding: `0 ${W * 0.08}px`,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: sora.fontFamily,\n            fontWeight: 400,\n            fontSize: nextSize,\n            lineHeight: 1.08,\n            letterSpacing: \"-0.035em\",\n            color: c.nextInk,\n            textAlign: \"center\",\n            textWrap: \"balance\",\n            maxWidth: W * 0.84,\n          }}\n        >\n          {nextWords.map((w, k) => {\n            const p = interpolate(\n              t,\n              [NEXT_AT + k * 3, NEXT_AT + k * 3 + 18],\n              [0, 1],\n              { ...CLAMP, easing: EASE_OUT },\n            );\n            return (\n              <span\n                key={`${w}-${k}`}\n                style={{\n                  display: \"inline-block\",\n                  overflow: \"hidden\",\n                  verticalAlign: \"top\",\n                  paddingBottom: \"0.08em\",\n                  marginRight: k < nextWords.length - 1 ? \"0.24em\" : 0,\n                }}\n              >\n                <span\n                  style={{\n                    display: \"inline-block\",\n                    transform: `translateY(${((1 - p) * 110).toFixed(1)}%)`,\n                  }}\n                >\n                  {w}\n                </span>\n              </span>\n            );\n          })}\n        </div>\n      </div>\n    </>\n  );\n\n  const lit = 1 - interpolate(t, [OPEN_AT - 2, OPEN_AT + 14], [0, 1], CLAMP);\n  const boost =\n    lit > 0\n      ? `brightness(${(1 + c.boost * lit).toFixed(3)}) saturate(${(1 + 0.25 * lit).toFixed(3)})`\n      : \"none\";\n\n  const crossClip = crossPolygon(\n    cx,\n    cy,\n    h0 * plusScale,\n    {\n      l: arms.l * (armH > 0 ? 1 : plusScale),\n      r: arms.r * (armH > 0 ? 1 : plusScale),\n      u: arms.u * (armV > 0 ? 1 : plusScale),\n      d: arms.d * (armV > 0 ? 1 : plusScale),\n    },\n    plusAngle,\n  );\n\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      {split ? (\n        <>\n          <div style={{ position: \"absolute\", inset: 0, filter: boost }}>\n            {nextScene}\n          </div>\n          {quarters}\n        </>\n      ) : (\n        <>\n          <div style={{ position: \"absolute\", inset: 0 }}>{oldScene}</div>\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              clipPath: crossClip,\n              opacity: plusOn,\n              filter: boost,\n            }}\n          >\n            {nextScene}\n          </div>\n        </>\n      )}\n      {bg === \"transparent\" ? null : <Grain opacity={c.grain} />}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/plus-open.tsx"
    }
  ],
  "type": "registry:component"
}
