{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select-menu",
  "title": "UI Select Menu",
  "description": "A vertical option list where exactly one row is highlighted; the highlight bar and left accent slide smoothly between rows. Fully controlled via selectedIndex/state or useSelectMenuTransition.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui"],
  "files": [
    {
      "path": "registry/remocn-ui/select-menu/index.tsx",
      "content": "\"use client\";\n\nimport { mixOklch, type RemocnTheme, useRemocnTheme } from \"@/lib/remocn-ui\";\n\nexport type SelectMenuState = string;\n\nexport type SelectMenuVariant = \"soft\" | \"solid\";\n\nexport interface SelectMenuProps {\n  options?: string[];\n  selectedIndex?: number;\n  state?: SelectMenuState;\n  style?: SelectMenuStyle;\n  variant?: SelectMenuVariant;\n  align?: \"start\" | \"center\" | \"end\";\n  theme?: Partial<RemocnTheme>;\n  className?: string;\n}\n\nconst WIDTH = 300;\nconst PAD = 4;\nconst ROW_HEIGHT = 44;\nconst ROW_GAP = 6;\nconst ACCENT_BAR = 3;\nconst LABEL_L_PAD = 18;\nconst CHECK_SIZE = 18;\nconst PRESS_DIP = 0.03;\nconst PRESS_TINT = 0.05;\n\nfunction justify(align: \"start\" | \"center\" | \"end\"): string {\n  return align === \"start\"\n    ? \"flex-start\"\n    : align === \"end\"\n      ? \"flex-end\"\n      : \"center\";\n}\n\nfunction clamp01(t: number): number {\n  return Math.max(0, Math.min(1, t));\n}\n\nconst DEFAULT_OPTIONS: string[] = [\"New Game\", \"Continue\", \"Settings\", \"Quit\"];\n\nexport interface SelectMenuStyle {\n  indicatorOffset: number;\n  press: number;\n  selectProgress: number;\n}\n\nexport interface SelectMenuStyleContext {\n  options: string[];\n  variant: SelectMenuVariant;\n  highlightBg: string;\n  highlightFg: string;\n  accent: string;\n  inactiveFg: string;\n  border: string;\n  radius: number;\n  foreground: string;\n}\n\nexport function selectMenuStyleContext(\n  options: string[],\n  variant: SelectMenuVariant,\n  theme: RemocnTheme,\n): SelectMenuStyleContext {\n  const soft = variant === \"soft\";\n  return {\n    options,\n    variant,\n    highlightBg: soft ? theme.accent : theme.primary,\n    highlightFg: soft ? theme.foreground : theme.primaryForeground,\n    accent: soft ? theme.primary : theme.primaryForeground,\n    inactiveFg: theme.mutedForeground,\n    border: theme.border,\n    radius: theme.radius,\n    foreground: theme.foreground,\n  };\n}\n\nexport function selectMenuStyle(\n  selectedIndex: number,\n  ctx: SelectMenuStyleContext,\n): SelectMenuStyle {\n  return {\n    indicatorOffset: Math.max(\n      0,\n      Math.min(ctx.options.length - 1, selectedIndex),\n    ),\n    press: 0,\n    selectProgress: 1,\n  };\n}\n\nexport function SelectMenu({\n  options = DEFAULT_OPTIONS,\n  selectedIndex,\n  state,\n  style,\n  variant = \"soft\",\n  align = \"center\",\n  theme: themeOverride,\n  className,\n}: SelectMenuProps) {\n  const theme = useRemocnTheme(themeOverride, \"light\");\n  const ctx = selectMenuStyleContext(options, variant, theme);\n\n  const snapIndex =\n    selectedIndex !== undefined\n      ? selectedIndex\n      : state !== undefined\n        ? options.indexOf(state)\n        : 0;\n  const v = style ?? selectMenuStyle(snapIndex, ctx);\n\n  const pressDepth = clamp01(v.press ?? 0);\n  const selectProgress = clamp01(v.selectProgress ?? 0);\n\n  const listHeight =\n    PAD * 2 + options.length * ROW_HEIGHT + (options.length - 1) * ROW_GAP;\n  const step = ROW_HEIGHT + ROW_GAP;\n\n  const highlightTop = PAD + v.indicatorOffset * step;\n  const highlightInset = PAD;\n\n  const isSoft = ctx.variant === \"soft\";\n  const highlightBackground = mixOklch(\n    ctx.highlightBg,\n    ctx.foreground,\n    PRESS_TINT * pressDepth,\n  );\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: justify(align),\n        background: \"transparent\",\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      <div\n        style={{\n          position: \"relative\",\n          width: WIDTH,\n          height: listHeight,\n          boxSizing: \"border-box\",\n        }}\n      >\n        {/* Outer track — an outline panel for the soft product look. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: ctx.radius,\n            border: isSoft ? `1px solid ${ctx.border}` : \"none\",\n          }}\n        />\n        {/* Sliding highlight: background + left accent bar */}\n        <div\n          style={{\n            position: \"absolute\",\n            top: highlightTop,\n            left: highlightInset,\n            width: WIDTH - highlightInset * 2,\n            height: ROW_HEIGHT,\n            boxSizing: \"border-box\",\n            borderRadius: Math.max(2, ctx.radius - 2),\n            background: highlightBackground,\n            border: isSoft ? `1px solid ${ctx.border}` : \"none\",\n            overflow: \"hidden\",\n          }}\n        >\n          {/* Left accent bar */}\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              bottom: 0,\n              width: ACCENT_BAR,\n              background: ctx.accent,\n            }}\n          />\n        </div>\n\n        {/* Rows */}\n        {options.map((option, i) => {\n          const proximity = Math.max(0, 1 - Math.abs(i - v.indicatorOffset));\n          const onRow = proximity >= 0.65 ? 1 : 0;\n          const scale = 1 - PRESS_DIP * pressDepth * proximity;\n          const proxGate = clamp01((proximity - 0.85) / 0.15);\n          const checkOpacity = selectProgress * proxGate;\n          return (\n            <div\n              key={option}\n              style={{\n                position: \"absolute\",\n                top: PAD + i * step,\n                left: highlightInset,\n                width: WIDTH - highlightInset * 2,\n                height: ROW_HEIGHT,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"space-between\",\n                paddingLeft: LABEL_L_PAD,\n                paddingRight: 12,\n                boxSizing: \"border-box\",\n                transform: `scale(${scale})`,\n                transformOrigin: \"center\",\n              }}\n            >\n              <span\n                style={{\n                  fontSize: 15,\n                  fontWeight: onRow ? 600 : 500,\n                  letterSpacing: \"0.01em\",\n                  color: mixOklch(ctx.inactiveFg, ctx.highlightFg, proximity),\n                }}\n              >\n                {option}\n              </span>\n              <svg\n                width={CHECK_SIZE}\n                height={CHECK_SIZE}\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                aria-hidden\n                style={{ flexShrink: 0, opacity: checkOpacity }}\n              >\n                <path\n                  d=\"M5 12.5l4.5 4.5L19 7\"\n                  stroke={ctx.highlightFg}\n                  strokeWidth=\"2.4\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                />\n              </svg>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/select-menu.tsx"
    },
    {
      "path": "registry/remocn-ui/select-menu/use-select-menu-transition.ts",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport {\n  type SelectMenuState,\n  type SelectMenuStyle,\n  type SelectMenuVariant,\n  selectMenuStyle,\n  selectMenuStyleContext,\n} from \"@/components/remocn/select-menu\";\nimport {\n  clamp01,\n  easings,\n  type RemocnTheme,\n  type Step,\n  useRemocnTheme,\n  useStateTransition,\n} from \"@/lib/remocn-ui\";\n\nconst DEFAULT_OPTIONS = [\"New Game\", \"Continue\", \"Settings\", \"Quit\"];\n\nexport const DEFAULT_DURATION = 14;\nexport const DEFAULT_DECAY_DURATION = 8;\nexport const PRESS_PORTION = 0.4;\n\nexport function tweenSelectMenuStyle(\n  a: SelectMenuStyle,\n  b: SelectMenuStyle,\n  t: number,\n): SelectMenuStyle {\n  return {\n    indicatorOffset:\n      a.indicatorOffset + (b.indicatorOffset - a.indicatorOffset) * t,\n    press: a.press + (b.press - a.press) * t,\n    selectProgress:\n      a.selectProgress + (b.selectProgress - a.selectProgress) * t,\n  };\n}\n\nexport interface SelectMenuClickOptions {\n  hold?: number;\n  decayDuration?: number;\n}\n\nexport function computeSelectMenuClick(\n  frame: number,\n  clickAt: number,\n  clickDuration = DEFAULT_DURATION,\n  opts: SelectMenuClickOptions = {},\n): Pick<SelectMenuStyle, \"press\" | \"selectProgress\"> {\n  const t = frame - clickAt;\n  const dur = Math.max(1, clickDuration);\n  const pressEnd = dur * PRESS_PORTION;\n\n  const press =\n    t <= 0 || t >= dur\n      ? 0\n      : t < pressEnd\n        ? clamp01(t / pressEnd)\n        : clamp01(1 - (t - pressEnd) / Math.max(1, dur - pressEnd));\n\n  const riseStart = pressEnd;\n  const rise = clamp01((t - riseStart) / Math.max(1, dur - riseStart));\n\n  let selectProgress = rise;\n  if (opts.hold !== undefined) {\n    const decayStart = dur + opts.hold;\n    const decay = clamp01(\n      (t - decayStart) /\n        Math.max(1, opts.decayDuration ?? DEFAULT_DECAY_DURATION),\n    );\n    selectProgress = clamp01(rise - decay);\n  }\n\n  return { press, selectProgress };\n}\n\nexport interface SelectMenuTransitionOptions extends SelectMenuClickOptions {\n  options?: string[];\n  variant?: SelectMenuVariant;\n  theme?: Partial<RemocnTheme>;\n  mode?: \"light\" | \"dark\";\n  speed?: number;\n  defaultDuration?: number;\n  clickAt?: number;\n  clickDuration?: number;\n}\n\nexport function useSelectMenuTransition(\n  steps: Step<SelectMenuState>[],\n  opts: SelectMenuTransitionOptions = {},\n): SelectMenuStyle {\n  const {\n    options = DEFAULT_OPTIONS,\n    variant = \"soft\",\n    theme: themeOverride,\n    mode,\n    speed = 1,\n    defaultDuration = DEFAULT_DURATION,\n    clickAt,\n    clickDuration = DEFAULT_DURATION,\n    hold,\n    decayDuration,\n  } = opts;\n  const theme = useRemocnTheme(themeOverride, mode);\n  const ctx = selectMenuStyleContext(options, variant, theme);\n  const { from, to, progress } = useStateTransition(\n    steps,\n    options[0] ?? \"\",\n    speed,\n    defaultDuration,\n  );\n  const t = easings.out(progress);\n  const style = tweenSelectMenuStyle(\n    selectMenuStyle(options.indexOf(from), ctx),\n    selectMenuStyle(options.indexOf(to), ctx),\n    t,\n  );\n\n  const frame = useCurrentFrame() * speed;\n  const click =\n    clickAt === undefined\n      ? { press: 0, selectProgress: 0 }\n      : computeSelectMenuClick(frame, clickAt, clickDuration, {\n          hold,\n          decayDuration,\n        });\n\n  return { ...style, ...click };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/use-select-menu-transition.ts"
    }
  ],
  "type": "registry:component"
}
