{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menu-pick",
  "title": "Menu Pick",
  "description": "A choose-your-launch menu dims two options and highlights the third on a red bar.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/menu-pick/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadRubik } from \"@remotion/google-fonts/Rubik\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\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 rubik = loadRubik(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface MenuPickProps {\n  prompt?: string;\n  options?: string[];\n  pick?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}\n\nconst RED = \"#E60012\";\n\nconst THEMES = {\n  dark: {\n    ink: \"#F4F2F1\",\n    soft: \"rgba(244,242,241,0.56)\",\n    faint: \"rgba(244,242,241,0.08)\",\n    onAccent: \"#FFFFFF\",\n    panel: \"rgba(16,13,14,0.9)\",\n    depth:\n      \"inset 0 0 0 1px rgba(255,255,255,0.07), inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 4px rgba(0,0,0,0.3), 0 24px 60px -12px rgba(0,0,0,0.6)\",\n    key: \"rgba(244,242,241,0.14)\",\n    ground: \"garnet\" as GshPresetId | undefined,\n    groundParams: undefined as Partial<GradientParams> | undefined,\n  },\n  light: {\n    ink: \"#141213\",\n    soft: \"rgba(20,18,19,0.52)\",\n    faint: \"rgba(20,18,19,0.07)\",\n    onAccent: \"#FFFFFF\",\n    panel: \"rgba(255,255,255,0.94)\",\n    depth:\n      \"inset 0 0 0 1px rgba(0,0,0,0.06), 0 1px 2px rgba(40,10,14,0.06), 0 24px 56px -16px rgba(60,12,20,0.22)\",\n    key: \"rgba(20,18,19,0.1)\",\n    ground: undefined as GshPresetId | undefined,\n    groundParams: {\n      kind: \"silk\",\n      colors: [\"#f0a8a0\", \"#fff4ef\", \"#ffc9c2\"],\n      background: \"#f3efec\",\n      loopSeconds: 28,\n      scale: 0.55,\n      warp: 0.3,\n      blur: 0.5,\n      seed: 2,\n    } as Partial<GradientParams> | undefined,\n  },\n};\n\nconst ROW_IN = 3;\nconst ROWS_AT = 4;\nconst CURSOR_AT = 14;\nconst HOP_GAPS = [10, 9, 7, 7];\nconst CONFIRM_GAP = 9;\nconst BLINK = 3;\nconst BLINKS = 2;\nconst DIM = 0.3;\n\nconst EASE_ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst EASE_MOVE = Easing.bezier(0.65, 0, 0.35, 1);\nconst SPRING_HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst SPRING_CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst SPRING_HOP = { mass: 0.5, damping: 16, stiffness: 260 };\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction estimateEm(text: string) {\n  let w = 0;\n  for (const ch of text) {\n    if (ch === \" \") w += 0.26;\n    else if (/[ilIj.,'’!|]/.test(ch)) w += 0.27;\n    else if (/[mwMW]/.test(ch)) w += 0.86;\n    else if (/[A-Z]/.test(ch)) w += 0.68;\n    else w += 0.56;\n  }\n  return w;\n}\n\nfunction hopRoute(count: number, pick: number) {\n  if (count <= 1) return [0];\n  const route = [0];\n  const near = Math.max(0, Math.min(count - 1, pick === 0 ? 1 : pick - 1));\n  const steps = [near, pick, near, pick];\n  for (const s of steps) if (s !== route[route.length - 1]) route.push(s);\n  if (route[route.length - 1] !== pick) route.push(pick);\n  return route;\n}\n\nfunction Chevron({ 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={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      <path d=\"M9 5.5 15.5 12 9 18.5\" vectorEffect=\"non-scaling-stroke\" />\n    </svg>\n  );\n}\n\nexport function MenuPick({\n  prompt = \"Choose your launch\",\n  options = [\"Keyframe every beat\", \"Wait on an agency\", \"Install the scenes\"],\n  pick = 2,\n  accent = RED,\n  background,\n  speed = 1,\n  theme = \"dark\",\n  ground,\n  groundParams,\n}: MenuPickProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const list = options.length > 0 ? options : [\"Start\"];\n  const n = list.length;\n  const chosen = Math.max(0, Math.min(n - 1, Math.round(pick)));\n  const transparent = background === \"transparent\";\n  const preset = ground ?? (groundParams ? undefined : c.ground);\n  const gParams = groundParams ?? c.groundParams;\n\n  const portrait = H > W * 1.1;\n  const panelW = portrait ? W * 0.86 : W * 0.6;\n  const panelPad = portrait ? W * 0.03 : W * 0.0125;\n  const panelR = panelPad + (portrait ? W * 0.022 : W * 0.0094);\n  const rowR = panelR - panelPad;\n  const rowPadX = portrait ? W * 0.04 : W * 0.022;\n  const chevS = portrait ? W * 0.05 : H * 0.042;\n  const widest = Math.max(...list.map(estimateEm));\n  const inner = panelW - panelPad * 2 - rowPadX * 2 - chevS * 1.6;\n  const F = Math.min(portrait ? W * 0.068 : H * 0.064, inner / widest);\n  const rowH = F * (portrait ? 2.2 : 2.05);\n  const smallFs = portrait ? W * 0.034 : H * 0.03;\n  const headH = smallFs * 3.6;\n  const footH = smallFs * 3.4;\n  const panelH = panelPad * 2 + headH + rowH * n + footH;\n  const panelX = (W - panelW) / 2;\n  const panelY = (H - panelH) / 2;\n  const rowsTop = panelPad + headH;\n  const rowW = panelW - panelPad * 2;\n\n  const route = hopRoute(n, chosen);\n  const hopAt = [CURSOR_AT];\n  for (let i = 1; i < route.length; i++) {\n    hopAt.push(hopAt[i - 1] + HOP_GAPS[Math.min(i - 1, HOP_GAPS.length - 1)]);\n  }\n  let pos = route[0] as number;\n  for (let i = 1; i < route.length; i++) {\n    const s = spring({ frame: t - hopAt[i], fps, config: SPRING_HOP });\n    pos += ((route[i] as number) - (route[i - 1] as number)) * s;\n  }\n  const confirmAt = (hopAt[hopAt.length - 1] as number) + CONFIRM_GAP;\n  const blinkEnd = confirmAt + BLINK * 2 * BLINKS;\n  const blinking = t >= confirmAt && t < blinkEnd;\n  const blinkOff = blinking && Math.floor((t - confirmAt) / BLINK) % 2 === 0;\n  const cursorIn = interpolate(t, [CURSOR_AT, CURSOR_AT + 6], [0, 1], {\n    ...CLAMP,\n    easing: EASE_ENTER,\n  });\n  const press =\n    spring({ frame: t - confirmAt, fps, config: SPRING_CHIP }) -\n    spring({ frame: t - confirmAt - 5, fps, config: SPRING_CHIP });\n  const barScale = 1 - 0.04 * press;\n  const settle = interpolate(t, [blinkEnd, blinkEnd + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASE_MOVE,\n  });\n\n  const panelIn = spring({ frame: t, fps, config: SPRING_HEAVY });\n  const panelO = interpolate(t, [0, 8], [0, 1], {\n    ...CLAMP,\n    easing: EASE_ENTER,\n  });\n  const headIn = interpolate(t, [2, 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE_ENTER,\n  });\n  const footIn = interpolate(\n    t,\n    [ROWS_AT + n * ROW_IN, ROWS_AT + n * ROW_IN + 12],\n    [0, 1],\n    {\n      ...CLAMP,\n      easing: EASE_ENTER,\n    },\n  );\n\n  const rowIn = (i: number) =>\n    interpolate(t, [ROWS_AT + i * ROW_IN, ROWS_AT + i * ROW_IN + 12], [0, 1], {\n      ...CLAMP,\n      easing: EASE_ENTER,\n    });\n\n  const barY = rowsTop + pos * rowH;\n\n  const rowText = (onBar: boolean) =>\n    list.map((label, i) => {\n      const p = rowIn(i);\n      const dim = i === chosen ? 1 : 1 - (1 - DIM) * settle;\n      return (\n        <div\n          key={`r${i}`}\n          style={{\n            position: \"absolute\",\n            left: panelPad,\n            top: rowsTop + i * rowH,\n            width: rowW,\n            height: rowH,\n            paddingLeft: rowPadX,\n            paddingRight: rowPadX * 0.7,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            opacity: p * (onBar ? 1 : dim),\n            transform: `translateY(${(1 - p) * rowH * 0.3}px)`,\n            color: onBar ? c.onAccent : c.ink,\n          }}\n        >\n          <span\n            style={{\n              fontFamily: rubik.fontFamily,\n              fontWeight: 400,\n              fontSize: F,\n              lineHeight: 1,\n              letterSpacing: \"-0.02em\",\n              whiteSpace: \"pre\",\n            }}\n          >\n            {label}\n          </span>\n          {onBar ? <Chevron size={chevS} color={c.onAccent} /> : null}\n        </div>\n      );\n    });\n\n  const Key = ({ k, label }: { k: string; label: string }) => (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: smallFs * 0.5,\n      }}\n    >\n      <span\n        style={{\n          width: smallFs * 1.5,\n          height: smallFs * 1.5,\n          borderRadius: 999,\n          backgroundColor: c.key,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: smallFs * 0.82,\n          color: c.ink,\n        }}\n      >\n        {k}\n      </span>\n      <span>{label}</span>\n    </div>\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: transparent ? \"transparent\" : background,\n        overflow: \"hidden\",\n      }}\n    >\n      {transparent || background ? null : (\n        <GshGround\n          preset={preset}\n          params={gParams}\n          timeOffset={theme === \"light\" ? 6 : 4}\n          timeScale={0.5}\n        />\n      )}\n      <div\n        style={{\n          position: \"absolute\",\n          left: panelX,\n          top: panelY,\n          width: panelW,\n          height: panelH,\n          borderRadius: panelR,\n          backgroundColor: c.panel,\n          boxShadow: c.depth,\n          opacity: panelO,\n          transform: `translateY(${(1 - panelIn) * H * 0.05}px) scale(${0.97 + 0.03 * panelIn})`,\n          fontFamily: rubik.fontFamily,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: panelPad + rowPadX,\n            right: panelPad + rowPadX,\n            top: panelPad,\n            height: headH,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            fontWeight: 400,\n            fontSize: smallFs,\n            letterSpacing: \"-0.005em\",\n            color: c.soft,\n            opacity: headIn,\n            transform: `translateY(${(1 - headIn) * smallFs * 0.6}px)`,\n          }}\n        >\n          <span style={{ color: c.ink }}>{prompt}</span>\n          <span style={{ fontVariantNumeric: \"tabular-nums\" }}>\n            {`${Math.min(n, Math.round(pos) + 1)} / ${n}`}\n          </span>\n        </div>\n        {list.slice(1).map((label, i) => (\n          <div\n            key={`d-${label}`}\n            style={{\n              position: \"absolute\",\n              left: panelPad + rowPadX,\n              right: panelPad + rowPadX,\n              top: rowsTop + (i + 1) * rowH,\n              height: 1,\n              backgroundColor: c.faint,\n              opacity: rowIn(i + 1),\n            }}\n          />\n        ))}\n        {rowText(false)}\n        <div\n          style={{\n            position: \"absolute\",\n            left: panelPad,\n            top: barY,\n            width: rowW,\n            height: rowH,\n            borderRadius: rowR,\n            overflow: \"hidden\",\n            backgroundColor: accent,\n            opacity: blinkOff ? 0 : cursorIn,\n            transform: `scale(${barScale * (0.96 + 0.04 * cursorIn)})`,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: -panelPad,\n              top: -barY,\n              width: panelW,\n              height: panelH,\n            }}\n          >\n            {rowText(true)}\n          </div>\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            left: panelPad + rowPadX,\n            right: panelPad + rowPadX,\n            bottom: panelPad,\n            height: footH,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            gap: smallFs * 1.6,\n            fontWeight: 400,\n            fontSize: smallFs,\n            color: c.soft,\n            opacity: footIn,\n          }}\n        >\n          <Key k=\"B\" label=\"Back\" />\n          <Key k=\"A\" label=\"Select\" />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/menu-pick.tsx"
    }
  ],
  "type": "registry:component"
}
