{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "idea-set",
  "title": "Idea Set",
  "description": "Two ruled lines set like a ledger, the second in an accent color: 'Napkin sketch. Keynote opener.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/idea-set/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadPlexMono } from \"@remotion/google-fonts/IBMPlexMono\";\nimport { loadFont as loadPlexSans } from \"@remotion/google-fonts/IBMPlexSans\";\nimport type React from \"react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst sans = loadPlexSans(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\nconst sansItalic = loadPlexSans(\"italic\", {\n  weights: [\"300\"],\n  subsets: [\"latin\"],\n});\nconst mono = loadPlexMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface IdeaSetProps {\n  lines?: string[];\n  highlight?: string;\n  hold?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#0F1B2D\",\n    ink: \"#F3F5F8\",\n    rule: \"rgba(243, 245, 248, 0.22)\",\n    accent: \"#FF9900\",\n  },\n  light: {\n    bg: \"#FFFFFF\",\n    ink: \"#0F1B2D\",\n    rule: \"rgba(15, 27, 45, 0.2)\",\n    accent: \"#C76A00\",\n  },\n} as const;\n\ninterface Face {\n  family: string;\n  style: \"normal\" | \"italic\";\n  weight: number;\n}\nconst FACES: Face[] = [\n  { family: sansItalic.fontFamily, style: \"italic\", weight: 300 },\n  { family: mono.fontFamily, style: \"normal\", weight: 400 },\n  { family: sans.fontFamily, style: \"normal\", weight: 500 },\n];\nconst TRACK = [0.012, 0, -0.03];\nconst LINE = 1.34;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ntype Advances = Record<string, [number, number, number]>;\ninterface Metrics {\n  adv: Advances;\n  asc: [number, number, number];\n  desc: [number, number, number];\n}\n\nfunction measure(chars: string[]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"idea-set: no 2d context\");\n  const fonts = FACES.map((f) => `${f.style} ${f.weight} 100px \"${f.family}\"`);\n  const adv: Advances = {};\n  for (const ch of chars) {\n    adv[ch] = fonts.map((font) => {\n      ctx.font = font;\n      return ctx.measureText(ch).width / 100;\n    }) as [number, number, number];\n  }\n  const asc = fonts.map((font) => {\n    ctx.font = font;\n    return ctx.measureText(\"Hg\").fontBoundingBoxAscent / 100;\n  }) as [number, number, number];\n  const desc = fonts.map((font) => {\n    ctx.font = font;\n    return ctx.measureText(\"Hg\").fontBoundingBoxDescent / 100;\n  }) as [number, number, number];\n  return { adv, asc, desc };\n}\n\nfunction useMetrics(chars: string[]) {\n  const key = chars.join(\"\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"idea-set: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      sans.waitUntilDone(),\n      sansItalic.waitUntilDone(),\n      mono.waitUntilDone(),\n      ...FACES.map((f) =>\n        document.fonts.load(`${f.style} ${f.weight} 100px \"${f.family}\"`),\n      ),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(Array.from(key)) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nexport function IdeaSet({\n  lines = [\"Napkin sketch.\", \"Keynote opener.\"],\n  highlight = \"Keynote opener.\",\n  hold = 18,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: IdeaSetProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const portrait = H > W * 1.1;\n  const rows = useMemo(() => {\n    const r = lines.map((l) => l.trim()).filter((l) => l.length > 0);\n    const base = r.length > 0 ? r : [\"remocn\"];\n    return portrait ? base.flatMap((l) => l.split(/\\s+/)) : base;\n  }, [lines, portrait]);\n  const chars = useMemo(\n    () => Array.from(new Set(Array.from(rows.join(\"\")))).sort(),\n    [rows],\n  );\n  const m = useMetrics(chars);\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const ENTER = 18;\n  const W1 = ENTER + hold;\n  const WAVE = 26;\n  const W2 = W1 + WAVE + hold;\n  const TURN = 10;\n\n  const joined = rows.join(\"\\n\");\n  const hlStart = highlight ? rows.join(\" \").indexOf(highlight) : -1;\n  const hlEnd = hlStart >= 0 ? hlStart + highlight.length : -1;\n\n  const total = Array.from(joined).filter((ch) => ch !== \"\\n\").length;\n  const stagger = Math.min(1.4, (WAVE - TURN) / Math.max(1, total - 1));\n\n  const widthIn = (row: string, s: number) => {\n    const g = Array.from(row);\n    let w = 0;\n    for (const ch of g) w += m.adv[ch][s];\n    return w + TRACK[s] * Math.max(0, g.length - 1);\n  };\n  const widest = Math.max(\n    ...rows.flatMap((r) => [widthIn(r, 0), widthIn(r, 1), widthIn(r, 2)]),\n  );\n  const margin = Math.min(W, H) * (portrait ? 0.08 : 0.09);\n  const F = Math.min(\n    (W - margin * 2) / widest,\n    (H * (portrait ? 0.62 : 0.66)) / (rows.length * LINE),\n    H * 0.2,\n  );\n  const lineH = F * LINE;\n  const baseIn = lineH * 0.78;\n  const visH = (rows.length - 1) * lineH + F * 1.02;\n  const top = (H - visH) / 2 - (baseIn - F * 0.72);\n\n  const stateOf = (i: number) => {\n    const a = interpolate(\n      t,\n      [W1 + i * stagger, W1 + i * stagger + TURN],\n      [0, 1],\n      { ...CLAMP, easing: EASE_IO },\n    );\n    const b = interpolate(\n      t,\n      [W2 + i * stagger, W2 + i * stagger + TURN],\n      [0, 1],\n      { ...CLAMP, easing: EASE_IO },\n    );\n    return { a, b };\n  };\n\n  const faceOf = (a: number, b: number) => (b >= 0.5 ? 2 : a >= 0.5 ? 1 : 0);\n  const advOf = (ch: string, a: number, b: number) => {\n    const A = m.adv[ch];\n    return b > 0 ? A[1] + (A[2] - A[1]) * b : A[0] + (A[1] - A[0]) * a;\n  };\n  const trackOf = (a: number, b: number) =>\n    b > 0\n      ? TRACK[1] + (TRACK[2] - TRACK[1]) * b\n      : TRACK[0] + (TRACK[1] - TRACK[0]) * a;\n\n  const els: React.ReactNode[] = [];\n  let gi = 0;\n  let ci = 0;\n  rows.forEach((row, r) => {\n    const g = Array.from(row);\n    const enter = interpolate(t, [r * 4, r * 4 + ENTER], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const ruleP = interpolate(t, [r * 3, r * 3 + ENTER + 4], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const baseY = top + r * lineH + baseIn;\n    let x = 0;\n    const rowGlyphs: React.ReactNode[] = [];\n    g.forEach((ch, k) => {\n      const { a, b } = stateOf(gi + k);\n      const fi = faceOf(a, b);\n      const face = FACES[fi];\n      const lift = Math.sin(Math.PI * a) + Math.sin(Math.PI * b);\n      const inHl = hlStart >= 0 && ci >= hlStart && ci < hlEnd;\n      const color = inHl && fi === 2 ? (accent ?? c.accent) : c.ink;\n      rowGlyphs.push(\n        <span\n          key={`g${r}-${k}`}\n          style={{\n            position: \"absolute\",\n            left: margin + x * F,\n            top: baseIn - m.asc[fi] * F,\n            lineHeight: 1,\n            height: (m.asc[fi] + m.desc[fi]) * F,\n            whiteSpace: \"pre\",\n            fontFamily: face.family,\n            fontStyle: face.style,\n            fontWeight: face.weight,\n            fontSize: F,\n            color,\n            transform: `translateY(${(-lift * F * 0.08).toFixed(2)}px)`,\n          }}\n        >\n          {ch}\n        </span>,\n      );\n      x += advOf(ch, a, b) + trackOf(a, b);\n      ci++;\n    });\n    gi += g.length;\n    ci++;\n    els.push(\n      <div\n        key={`rule${r}`}\n        style={{\n          position: \"absolute\",\n          left: margin,\n          top: baseY + F * 0.3,\n          width: (W - margin * 2) * ruleP,\n          height: Math.max(1, F * 0.012),\n          background: c.rule,\n        }}\n      />,\n    );\n    els.push(\n      <div\n        key={`row${r}`}\n        style={{\n          position: \"absolute\",\n          left: 0,\n          width: W,\n          top: top + r * lineH,\n          height: baseIn + F * 0.28,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: (1 - enter) * lineH,\n            width: W,\n            height: lineH,\n          }}\n        >\n          {rowGlyphs}\n        </div>\n      </div>,\n    );\n  });\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {els}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/idea-set.tsx"
    }
  ],
  "type": "registry:component"
}
