{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "keystroke",
  "title": "Keystroke",
  "description": "Physical keycaps rise in for each shortcut, press in real order with the modifiers held while the letter taps, spring back with a small capped overshoot, and leave after an idle hold, firing on the same frames as the remocn-ui steps they trigger.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/keystroke/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Easing, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type KeystrokePlatform = \"mac\" | \"windows\";\nexport type KeystrokeTheme = \"light\" | \"dark\";\nexport type KeystrokeIcon =\n  | \"command\"\n  | \"option\"\n  | \"shift\"\n  | \"control\"\n  | \"return\"\n  | \"delete\"\n  | \"escape\"\n  | \"tab\"\n  | \"space\"\n  | \"capsLock\"\n  | \"up\"\n  | \"down\"\n  | \"left\"\n  | \"right\";\n\nexport interface KeystrokeStep {\n  at: number;\n  keys: string;\n  hold?: number;\n}\n\nexport interface KeystrokeProps {\n  steps?: readonly KeystrokeStep[];\n  platform?: KeystrokePlatform;\n  theme?: KeystrokeTheme;\n  accent?: string;\n  x?: number;\n  y?: number;\n  size?: number;\n  linger?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport interface KeystrokeOptions extends Omit<KeystrokeProps, \"className\"> {\n  width?: number;\n  height?: number;\n  fps?: number;\n}\n\nexport type KeystrokeLegend =\n  | { kind: \"icon\"; icon: KeystrokeIcon }\n  | { kind: \"char\"; text: string }\n  | { kind: \"word\"; text: string };\n\nexport interface KeystrokeKey {\n  name: string;\n  label: string;\n  legend: KeystrokeLegend;\n  units: number;\n  modifier: boolean;\n}\n\nexport type KeystrokeChord = KeystrokeKey[];\n\nexport interface KeystrokeKeyTiming {\n  press: number;\n  release: number;\n}\n\nexport interface KeystrokeChordTiming {\n  enter: number;\n  fire: number;\n  keys: KeystrokeKeyTiming[];\n}\n\nexport interface KeystrokeLayout {\n  caps: { chord: number; index: number; x: number; width: number }[];\n  separators: { chord: number; x: number; width: number }[];\n  widths: number[];\n}\n\nexport interface KeystrokeGroup {\n  index: number;\n  step: number;\n  keys: string;\n  label: string;\n  chords: KeystrokeChord[];\n  timing: KeystrokeChordTiming[];\n  layout: KeystrokeLayout;\n  at: number;\n  hold: number;\n  enter: number;\n  lastRelease: number;\n  pushes: number[];\n  exit: number;\n  end: number;\n}\n\nexport interface KeystrokePalette {\n  cap: string;\n  edge: string;\n  wall: string;\n  ink: string;\n  separator: string;\n  shadow: string;\n}\n\ntype Unset = \"steps\" | \"className\";\ntype Modifier = \"fn\" | \"ctrl\" | \"alt\" | \"shift\" | \"meta\";\ntype Named =\n  | \"enter\"\n  | \"escape\"\n  | \"tab\"\n  | \"space\"\n  | \"backspace\"\n  | \"delete\"\n  | \"capsLock\"\n  | \"up\"\n  | \"down\"\n  | \"left\"\n  | \"right\"\n  | \"home\"\n  | \"end\"\n  | \"pageUp\"\n  | \"pageDown\";\ntype IconShape = { box: string; paths: readonly string[] };\ntype IconSpec = { label: string; icon: KeystrokeIcon; units: number };\ntype NamedSpec = IconSpec | { label: string; word: string; units: number };\ntype Draft = Omit<\n  KeystrokeGroup,\n  \"index\" | \"label\" | \"layout\" | \"pushes\" | \"exit\" | \"end\"\n>;\n\nexport const keystrokeDefaults: Required<Omit<KeystrokeProps, Unset>> = {\n  platform: \"mac\",\n  theme: \"light\",\n  accent: \"#0a84ff\",\n  x: 0.5,\n  y: 0.86,\n  size: 64,\n  linger: 30,\n  speed: 1,\n};\n\nexport const keystrokeDefaultSteps: readonly KeystrokeStep[] = [\n  { at: 20, keys: \"mod+k\" },\n  { at: 78, keys: \"g i\" },\n  { at: 114, keys: \"shift+mod+p\" },\n];\n\nexport const keystrokePalettes: Record<KeystrokeTheme, KeystrokePalette> = {\n  light: {\n    cap: \"#fcfcfb\",\n    edge: \"#dfdfda\",\n    wall: \"#cfcfca\",\n    ink: \"#1d1d1f\",\n    separator: \"#8e8e89\",\n    shadow: \"rgba(20, 20, 16, 0.18)\",\n  },\n  dark: {\n    cap: \"#38393e\",\n    edge: \"#48494f\",\n    wall: \"#1f2023\",\n    ink: \"#f5f5f7\",\n    separator: \"#9d9da4\",\n    shadow: \"rgba(0, 0, 0, 0.5)\",\n  },\n};\n\nconst PRESS = 3;\nconst PRESS_GAP = 3;\nconst RELEASE_GAP = 3;\nconst LEAD = 7;\nconst STAGGER = 1;\nconst BEAT = 2;\nconst HOLD = 8;\nconst RELEASE = 10;\nconst RELEASE_RISE = 3;\nconst RELEASE_TAPER = 6;\nconst RELEASE_DAMPING = 0.46;\nconst ENTER = 10;\nconst ENTER_RISE = 4;\nconst ENTER_TAPER = 7;\nconst ENTER_DAMPING = 0.65;\nconst FADE_IN = 4;\nconst SLIDE = 10;\nconst DIM = 6;\nconst DIM_OPACITY = 0.45;\nconst PUSH = 10;\nconst BRIEF = 18;\nconst EXIT = 8;\nconst TAIL = 14;\nconst MAX_FRAMES = 600;\nconst MIN_SIZE = 8;\nconst MAX_SIZE = 400;\nconst WALL = 0.16;\nconst TRAVEL = 0.12;\nconst RADIUS = 0.16;\nconst RISE = 0.42;\nconst KEY_GAP = 0.14;\nconst SEPARATOR = 0.95;\nconst CHAR_FONT = 0.4;\nconst WORD_FONT = 0.26;\nconst ICON_SIZE = 0.36;\nconst SEPARATOR_FONT = 0.25;\nconst WORD_CHAR = 0.6 * WORD_FONT;\nconst WORD_PAD = 0.24;\nconst WINDOWS_WORD = 1.25;\nconst SLOT_RISE = 1.09;\nconst SLOT_SCALE = 0.86;\nconst SLOT_OPACITY = 0.55;\nconst EXIT_DRIFT = 0.22;\nconst EXIT_SCALE = 0.04;\nconst SHADOW_Y = 0.07;\nconst SHADOW_Y_PRESSED = 0.02;\nconst SHADOW_BLUR = 0.11;\nconst SHADOW_BLUR_PRESSED = 0.03;\nconst GLIDE = Easing.bezier(0.33, 1, 0.68, 1);\nconst FONT_FAMILY =\n  'var(--font-geist-sans, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Inter, Roboto, Helvetica, Arial, sans-serif)';\n\nconst ICONS: Record<KeystrokeIcon, IconShape> = {\n  command: {\n    box: \"0 0 24 24\",\n    paths: [\n      \"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3\",\n    ],\n  },\n  option: { box: \"0 0 24 24\", paths: [\"M3 3h6l6 18h6\", \"M14 3h7\"] },\n  shift: {\n    box: \"0 0 24 24\",\n    paths: [\n      \"M9 19a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-6a1 1 0 0 1 1-1h3.293a.707.707 0 0 0 .5-1.207l-7.086-7.086a1 1 0 0 0-1.414 0l-7.086 7.086a.707.707 0 0 0 .5 1.207H8a1 1 0 0 1 1 1z\",\n    ],\n  },\n  control: { box: \"0 0 24 24\", paths: [\"m18 15-6-6-6 6\"] },\n  return: {\n    box: \"0 0 24 24\",\n    paths: [\"M20 4v7a4 4 0 0 1-4 4H4\", \"m9 10-5 5 5 5\"],\n  },\n  delete: {\n    box: \"0 0 24 24\",\n    paths: [\n      \"M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z\",\n      \"m12 9 6 6\",\n      \"m18 9-6 6\",\n    ],\n  },\n  escape: {\n    box: \"0 0 24 24\",\n    paths: [\"M2 8V2h6\", \"m2 2 10 10\", \"M12 2A10 10 0 1 1 2 12\"],\n  },\n  tab: {\n    box: \"0 0 24 24\",\n    paths: [\"M17 12H3\", \"m11 18 6-6-6-6\", \"M21 5v14\"],\n  },\n  space: {\n    box: \"0 6 24 24\",\n    paths: [\"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1\"],\n  },\n  capsLock: {\n    box: \"0 0 24 24\",\n    paths: [\n      \"M14 16a1 1 0 0 0 1-1v-2a1 1 0 0 1 1-1h3.293a.707.707 0 0 0 .5-1.207l-6.939-6.939a1.207 1.207 0 0 0-1.708 0l-6.94 6.94a.707.707 0 0 0 .5 1.206H8a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1z\",\n      \"M9 20h6\",\n    ],\n  },\n  up: { box: \"0 0 24 24\", paths: [\"m5 12 7-7 7 7\", \"M12 19V5\"] },\n  down: { box: \"0 0 24 24\", paths: [\"M12 5v14\", \"m19 12-7 7-7-7\"] },\n  left: { box: \"0 0 24 24\", paths: [\"m12 19-7-7 7-7\", \"M19 12H5\"] },\n  right: { box: \"0 0 24 24\", paths: [\"M5 12h14\", \"m12 5 7 7-7 7\"] },\n};\n\nconst MODIFIERS = new Map<string, Modifier>([\n  [\"fn\", \"fn\"],\n  [\"ctrl\", \"ctrl\"],\n  [\"control\", \"ctrl\"],\n  [\"alt\", \"alt\"],\n  [\"option\", \"alt\"],\n  [\"opt\", \"alt\"],\n  [\"shift\", \"shift\"],\n  [\"cmd\", \"meta\"],\n  [\"command\", \"meta\"],\n  [\"meta\", \"meta\"],\n  [\"win\", \"meta\"],\n  [\"super\", \"meta\"],\n]);\n\nconst MODIFIER_ORDER: Record<KeystrokePlatform, readonly Modifier[]> = {\n  mac: [\"fn\", \"ctrl\", \"alt\", \"shift\", \"meta\"],\n  windows: [\"fn\", \"meta\", \"ctrl\", \"alt\", \"shift\"],\n};\n\nconst MAC_MODIFIERS: Record<Exclude<Modifier, \"fn\">, IconSpec> = {\n  ctrl: { label: \"Control\", icon: \"control\", units: 1 },\n  alt: { label: \"Option\", icon: \"option\", units: 1 },\n  shift: { label: \"Shift\", icon: \"shift\", units: 1.5 },\n  meta: { label: \"Command\", icon: \"command\", units: 1 },\n};\n\nconst WINDOWS_MODIFIERS: Record<Modifier, string> = {\n  fn: \"Fn\",\n  ctrl: \"Ctrl\",\n  alt: \"Alt\",\n  shift: \"Shift\",\n  meta: \"Win\",\n};\n\nconst NAMED = new Map<string, Named>([\n  [\"enter\", \"enter\"],\n  [\"return\", \"enter\"],\n  [\"esc\", \"escape\"],\n  [\"escape\", \"escape\"],\n  [\"tab\", \"tab\"],\n  [\"space\", \"space\"],\n  [\"backspace\", \"backspace\"],\n  [\"delete\", \"delete\"],\n  [\"del\", \"delete\"],\n  [\"capslock\", \"capsLock\"],\n  [\"up\", \"up\"],\n  [\"arrowup\", \"up\"],\n  [\"down\", \"down\"],\n  [\"arrowdown\", \"down\"],\n  [\"left\", \"left\"],\n  [\"arrowleft\", \"left\"],\n  [\"right\", \"right\"],\n  [\"arrowright\", \"right\"],\n  [\"home\", \"home\"],\n  [\"end\", \"end\"],\n  [\"pageup\", \"pageUp\"],\n  [\"pgup\", \"pageUp\"],\n  [\"pagedown\", \"pageDown\"],\n  [\"pgdn\", \"pageDown\"],\n]);\n\nconst CHARACTERS = new Map<string, string>([\n  [\"plus\", \"+\"],\n  [\"minus\", \"-\"],\n  [\"comma\", \",\"],\n  [\"period\", \".\"],\n  [\"slash\", \"/\"],\n  [\"backslash\", \"\\\\\"],\n  [\"semicolon\", \";\"],\n  [\"quote\", \"'\"],\n  [\"backquote\", \"`\"],\n  [\"bracketleft\", \"[\"],\n  [\"bracketright\", \"]\"],\n  [\"equal\", \"=\"],\n]);\n\nconst ARROWS: Record<\"up\" | \"down\" | \"left\" | \"right\", IconSpec> = {\n  up: { label: \"Up Arrow\", icon: \"up\", units: 1 },\n  down: { label: \"Down Arrow\", icon: \"down\", units: 1 },\n  left: { label: \"Left Arrow\", icon: \"left\", units: 1 },\n  right: { label: \"Right Arrow\", icon: \"right\", units: 1 },\n};\n\nconst NAMED_KEYS: Record<KeystrokePlatform, Record<Named, NamedSpec>> = {\n  mac: {\n    enter: { label: \"Return\", icon: \"return\", units: 1.5 },\n    escape: { label: \"Escape\", icon: \"escape\", units: 1 },\n    tab: { label: \"Tab\", icon: \"tab\", units: 1.25 },\n    space: { label: \"Space\", icon: \"space\", units: 2.5 },\n    backspace: { label: \"Delete\", icon: \"delete\", units: 1.25 },\n    delete: { label: \"Delete\", icon: \"delete\", units: 1.25 },\n    capsLock: { label: \"Caps Lock\", icon: \"capsLock\", units: 1.5 },\n    ...ARROWS,\n    home: { label: \"Home\", word: \"home\", units: 1 },\n    end: { label: \"End\", word: \"end\", units: 1 },\n    pageUp: { label: \"Page Up\", word: \"page up\", units: 1 },\n    pageDown: { label: \"Page Down\", word: \"page down\", units: 1 },\n  },\n  windows: {\n    enter: { label: \"Enter\", word: \"Enter\", units: WINDOWS_WORD },\n    escape: { label: \"Esc\", word: \"Esc\", units: WINDOWS_WORD },\n    tab: { label: \"Tab\", word: \"Tab\", units: WINDOWS_WORD },\n    space: { label: \"Space\", word: \"Space\", units: 2.5 },\n    backspace: { label: \"Backspace\", word: \"Backspace\", units: WINDOWS_WORD },\n    delete: { label: \"Delete\", word: \"Delete\", units: WINDOWS_WORD },\n    capsLock: { label: \"Caps Lock\", word: \"Caps Lock\", units: WINDOWS_WORD },\n    ...ARROWS,\n    home: { label: \"Home\", word: \"Home\", units: WINDOWS_WORD },\n    end: { label: \"End\", word: \"End\", units: WINDOWS_WORD },\n    pageUp: { label: \"Page Up\", word: \"PgUp\", units: WINDOWS_WORD },\n    pageDown: { label: \"Page Down\", word: \"PgDn\", units: WINDOWS_WORD },\n  },\n};\n\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst within = (\n  value: number | undefined,\n  fallback: number,\n  min: number,\n  max: number,\n) => clamp(finite(value, fallback), min, max);\nconst round = (value: number) => Math.round(value * 1000) / 1000 + 0;\nconst smoothstep = (value: number) => {\n  const t = clamp(value, 0, 1);\n  return t * t * (3 - 2 * t);\n};\nconst fpsOf = (fps: number | undefined) => Math.max(1, finite(fps, 30));\nconst rateOf = (speed: number | undefined) =>\n  Math.max(0, finite(speed, keystrokeDefaults.speed));\nconst platformOf = (value: string | undefined): KeystrokePlatform =>\n  value === \"windows\" ? value : \"mac\";\nconst themeOf = (value: string | undefined): KeystrokeTheme =>\n  value === \"dark\" ? value : \"light\";\nconst accentOf = (value: string | undefined) =>\n  typeof value === \"string\" && value.trim() !== \"\"\n    ? value.trim()\n    : keystrokeDefaults.accent;\nconst modOf = (platform: KeystrokePlatform): Modifier =>\n  platform === \"mac\" ? \"meta\" : \"ctrl\";\n\nfunction eased(frames: number, duration: number, curve: (t: number) => number) {\n  if (Number.isNaN(frames) || frames <= 0) return 0;\n  if (frames >= duration) return 1;\n  return curve(frames / duration);\n}\n\nfunction settle(rise: number, damping: number, taper: number, end: number) {\n  const frequency = (Math.PI - Math.acos(damping)) / rise;\n  const ratio = damping / Math.sqrt(1 - damping ** 2);\n  return (frames: number) => {\n    if (Number.isNaN(frames) || frames <= 0) return 0;\n    if (frames >= end) return 1;\n    const phase = frequency * frames;\n    const decay = Math.exp(-ratio * phase);\n    const ring = decay * (Math.cos(phase) + ratio * Math.sin(phase));\n    return 1 - ring * (1 - smoothstep((frames - taper) / (end - taper)));\n  };\n}\n\nconst riseCurve = settle(ENTER_RISE, ENTER_DAMPING, ENTER_TAPER, ENTER);\nconst releaseCurve = settle(\n  RELEASE_RISE,\n  RELEASE_DAMPING,\n  RELEASE_TAPER,\n  RELEASE,\n);\n\nexport function getKeystrokePress(frames: number) {\n  return eased(frames, PRESS, smoothstep);\n}\n\nexport function getKeystrokeRelease(frames: number) {\n  return releaseCurve(frames);\n}\n\nexport function getKeystrokeRise(frames: number) {\n  return riseCurve(frames);\n}\n\nexport function getKeystrokeDepth(time: number, timing: KeystrokeKeyTiming) {\n  if (Number.isNaN(time) || time <= timing.press) return 0;\n  if (time < timing.release) return getKeystrokePress(time - timing.press);\n  const bottom = getKeystrokePress(timing.release - timing.press);\n  return bottom * (1 - getKeystrokeRelease(time - timing.release));\n}\n\nexport function getKeystrokeWordUnits(text: string, min = 1) {\n  const raw = Array.from(text).length * WORD_CHAR + 2 * WORD_PAD;\n  return Math.max(min, Math.ceil(raw * 4 - 1e-9) / 4);\n}\n\nfunction wordKey(\n  name: string,\n  label: string,\n  text: string,\n  platform: KeystrokePlatform,\n  modifier: boolean,\n  min = 1,\n): KeystrokeKey {\n  const floor = platform === \"windows\" ? Math.max(min, WINDOWS_WORD) : min;\n  const units = getKeystrokeWordUnits(text, floor);\n  return { name, label, legend: { kind: \"word\", text }, units, modifier };\n}\n\nfunction modifierKey(\n  modifier: Modifier,\n  platform: KeystrokePlatform,\n): KeystrokeKey {\n  if (platform === \"windows\") {\n    const word = WINDOWS_MODIFIERS[modifier];\n    return wordKey(modifier, word, word, platform, true);\n  }\n  if (modifier === \"fn\") return wordKey(\"fn\", \"Fn\", \"fn\", platform, true);\n  const { label, icon, units } = MAC_MODIFIERS[modifier];\n  return {\n    name: modifier,\n    label,\n    legend: { kind: \"icon\", icon },\n    units,\n    modifier: true,\n  };\n}\n\nfunction namedKey(named: Named, platform: KeystrokePlatform): KeystrokeKey {\n  const spec = NAMED_KEYS[platform][named];\n  if (\"word\" in spec) {\n    return wordKey(named, spec.label, spec.word, platform, false, spec.units);\n  }\n  return {\n    name: named,\n    label: spec.label,\n    legend: { kind: \"icon\", icon: spec.icon },\n    units: spec.units,\n    modifier: false,\n  };\n}\n\nfunction keyOf(part: string, platform: KeystrokePlatform): KeystrokeKey {\n  const name = part.toLowerCase();\n  const named = NAMED.get(name);\n  if (named) return namedKey(named, platform);\n  const character =\n    CHARACTERS.get(name) ?? (Array.from(part).length === 1 ? part : undefined);\n  if (character !== undefined) {\n    const text = character.toUpperCase();\n    return {\n      name: character.toLowerCase(),\n      label: text,\n      legend: { kind: \"char\", text },\n      units: 1,\n      modifier: false,\n    };\n  }\n  const fkey = /^f([1-9]|1\\d|2[0-4])$/.exec(name);\n  if (fkey) {\n    const text = `F${fkey[1]}`;\n    return wordKey(name, text, text, platform, false);\n  }\n  const text = `${part.charAt(0).toUpperCase()}${part.slice(1)}`;\n  return wordKey(name, text, text, platform, false);\n}\n\nfunction splitChord(token: string) {\n  const parts = token.split(\"+\");\n  const names: string[] = [];\n  for (const [index, part] of parts.entries()) {\n    if (part !== \"\") names.push(part);\n    else if (index === parts.length - 1 && parts[index - 1] === \"\") {\n      names.push(\"+\");\n    }\n  }\n  return names;\n}\n\nfunction parseChord(token: string, platform: KeystrokePlatform) {\n  const held = new Set<Modifier>();\n  const keys: KeystrokeKey[] = [];\n  for (const part of splitChord(token)) {\n    const name = part.toLowerCase();\n    const modifier = name === \"mod\" ? modOf(platform) : MODIFIERS.get(name);\n    if (modifier) held.add(modifier);\n    else keys.push(keyOf(part, platform));\n  }\n  const modifiers = MODIFIER_ORDER[platform]\n    .filter((modifier) => held.has(modifier))\n    .map((modifier) => modifierKey(modifier, platform));\n  return [...modifiers, ...keys];\n}\n\nexport function parseKeystrokeKeys(\n  keys: string,\n  platform?: KeystrokePlatform,\n): KeystrokeChord[] {\n  if (typeof keys !== \"string\") return [];\n  const target = platformOf(platform);\n  return keys\n    .trim()\n    .split(/\\s+/)\n    .filter((token) => token !== \"\")\n    .map((token) => parseChord(token, target))\n    .filter((chord) => chord.length > 0);\n}\n\nexport function getKeystrokeLayout(\n  chords: readonly KeystrokeChord[],\n): KeystrokeLayout {\n  const layout: KeystrokeLayout = { caps: [], separators: [], widths: [] };\n  let cursor = 0;\n  for (const [chord, keys] of chords.entries()) {\n    if (chord > 0) {\n      layout.separators.push({ chord, x: round(cursor), width: SEPARATOR });\n      cursor += SEPARATOR;\n    }\n    for (const [index, key] of keys.entries()) {\n      if (index > 0) cursor += KEY_GAP;\n      layout.caps.push({ chord, index, x: round(cursor), width: key.units });\n      cursor += key.units;\n    }\n    layout.widths.push(round(cursor));\n  }\n  return layout;\n}\n\nfunction schedule(chords: readonly KeystrokeChord[], hold: number) {\n  const timing: KeystrokeChordTiming[] = [];\n  let enter = 0;\n  for (const chord of chords) {\n    const last = chord.length - 1;\n    const fire = enter + LEAD + PRESS_GAP * last + PRESS;\n    const keys = chord.map((_, index) => ({\n      press: fire - PRESS - PRESS_GAP * (last - index),\n      release: fire + hold + RELEASE_GAP * (last - index),\n    }));\n    timing.push({ enter, fire, keys });\n    enter = fire + hold + RELEASE_GAP * last + BEAT;\n  }\n  return timing;\n}\n\nexport function getKeystrokeLeadIn(keys: string, hold = HOLD) {\n  const chords = parseKeystrokeKeys(keys);\n  if (chords.length === 0) return 0;\n  const timing = schedule(chords, within(hold, HOLD, 0, MAX_FRAMES));\n  return timing[timing.length - 1].fire - timing[0].enter;\n}\n\nexport function getKeystrokeTimeline(\n  options: Pick<KeystrokeOptions, \"steps\" | \"platform\" | \"linger\" | \"fps\"> = {},\n) {\n  const platform = platformOf(options.platform);\n  const scale = 30 / fpsOf(options.fps);\n  const lingerFrames = within(\n    options.linger,\n    keystrokeDefaults.linger,\n    0,\n    MAX_FRAMES,\n  );\n  const linger = lingerFrames * scale;\n  const steps = options.steps ?? keystrokeDefaultSteps;\n  const drafts: Draft[] = [];\n  for (const [step, entry] of steps.entries()) {\n    if (typeof entry.at !== \"number\" || !Number.isFinite(entry.at)) continue;\n    const chords = parseKeystrokeKeys(entry.keys, platform);\n    if (chords.length === 0) continue;\n    const at = entry.at * scale;\n    const hold = within(entry.hold, HOLD, 0, MAX_FRAMES) * scale;\n    const planned = schedule(chords, hold);\n    const offset = at - planned[planned.length - 1].fire;\n    const timing = planned.map((chord) => ({\n      enter: round(chord.enter + offset),\n      fire: round(chord.fire + offset),\n      keys: chord.keys.map((key) => ({\n        press: round(key.press + offset),\n        release: round(key.release + offset),\n      })),\n    }));\n    const releases = timing.flatMap((chord) =>\n      chord.keys.map((key) => key.release),\n    );\n    drafts.push({\n      step,\n      keys: entry.keys,\n      chords,\n      timing,\n      at: round(at),\n      hold: round(hold),\n      enter: timing[0].enter,\n      lastRelease: Math.max(...releases),\n    });\n  }\n  drafts.sort((a, b) => a.enter - b.enter || a.at - b.at || a.step - b.step);\n  const groups = drafts.map((draft, index): KeystrokeGroup => {\n    const pushes: number[] = [];\n    let exit = Math.max(draft.enter, draft.lastRelease + linger);\n    for (const newer of drafts.slice(index + 1)) {\n      if (newer.enter >= exit + EXIT) break;\n      const at = Math.max(newer.enter, draft.enter);\n      const brief = Math.max(at, draft.lastRelease) + BRIEF;\n      pushes.push(at);\n      exit = Math.min(exit, pushes.length === 1 ? brief : at);\n      if (pushes.length === 2) break;\n    }\n    exit = round(Math.max(exit, draft.enter));\n    const label = draft.chords\n      .map((chord) => chord.map((key) => key.label).join(\" \"))\n      .join(\" then \");\n    return {\n      ...draft,\n      index,\n      label,\n      layout: getKeystrokeLayout(draft.chords),\n      pushes,\n      exit,\n      end: round(exit + EXIT),\n    };\n  });\n  const end = groups.reduce((latest, group) => Math.max(latest, group.end), 0);\n  return { platform, linger: round(linger), groups, end };\n}\n\nexport type KeystrokeTimeline = ReturnType<typeof getKeystrokeTimeline>;\n\nexport const keystrokeLength = getKeystrokeTimeline().end;\n\nexport function getKeystrokeDuration(\n  options: Pick<KeystrokeOptions, \"steps\" | \"linger\" | \"speed\" | \"fps\"> = {},\n) {\n  const fps = fpsOf(options.fps);\n  const rate = rateOf(options.speed);\n  const { end } = getKeystrokeTimeline(options);\n  const motion = rate === 0 ? 0 : (end * fps) / 30 / rate;\n  return Math.ceil(round(motion)) + Math.round((TAIL * fps) / 30);\n}\n\nexport function getKeystrokeTime(\n  frame: number,\n  options: Pick<KeystrokeOptions, \"fps\" | \"speed\"> = {},\n) {\n  const elapsed = Math.max(0, finite(frame, 0));\n  return (elapsed * rateOf(options.speed) * 30) / fpsOf(options.fps);\n}\n\nexport function getKeystrokeMetrics(size: number, unit = 1) {\n  return {\n    size: round(size),\n    height: round(size),\n    wall: round(size * WALL),\n    travel: round(size * TRAVEL),\n    keytop: round(size * (1 - WALL)),\n    radius: round(size * RADIUS),\n    rise: round(size * RISE),\n    border: round(Math.max(1, unit)),\n    charFont: round(size * CHAR_FONT),\n    wordFont: round(size * WORD_FONT),\n    icon: round(size * ICON_SIZE),\n    separatorFont: round(size * SEPARATOR_FONT),\n  };\n}\n\nexport type KeystrokeMetrics = ReturnType<typeof getKeystrokeMetrics>;\n\nfunction legendColor(ink: string, accent: string, amount: number) {\n  const share = Math.floor(clamp(amount, 0, 1) * 100);\n  if (share <= 0) return ink;\n  if (share >= 100) return accent;\n  return `color-mix(in srgb, ${accent} ${share}%, ${ink})`;\n}\n\nfunction fadeIn(frames: number) {\n  return eased(frames, FADE_IN, (t) => 1 - (1 - t) ** 2);\n}\n\nfunction groupState(\n  group: KeystrokeGroup,\n  time: number,\n  metrics: KeystrokeMetrics,\n  palette: KeystrokePalette,\n  accent: string,\n) {\n  const { size } = metrics;\n  const slot = group.pushes.reduce(\n    (sum, at) => sum + eased(time - at, PUSH, GLIDE),\n    0,\n  );\n  const leaving = clamp((time - group.exit) / EXIT, 0, 1);\n  const pushedAway = group.pushes.some((at) => at <= group.exit);\n  const drift = (pushedAway ? -1 : 1) * EXIT_DRIFT * size * leaving ** 3;\n  const dimmed = 1 - (1 - SLOT_OPACITY) * Math.min(1, slot);\n  const last = group.chords.length - 1;\n  let visible = group.layout.widths[0];\n  for (let chord = 1; chord <= last; chord++) {\n    const added = group.layout.widths[chord] - group.layout.widths[chord - 1];\n    visible += added * eased(time - group.timing[chord].enter, SLIDE, GLIDE);\n  }\n  const left = -visible / 2;\n  const caps = group.layout.caps.flatMap((cap) => {\n    const timing = group.timing[cap.chord];\n    const enter = timing.enter + cap.index * STAGGER;\n    if (time <= enter) return [];\n    const key = group.chords[cap.chord][cap.index];\n    const depth = getKeystrokeDepth(time, timing.keys[cap.index]);\n    const landing =\n      group.timing.at(cap.chord + 1)?.fire ?? Number.POSITIVE_INFINITY;\n    const dim = smoothstep((time - (landing - DIM)) / DIM);\n    const lean = clamp(depth, -1, 1);\n    const shadowY = SHADOW_Y - (SHADOW_Y - SHADOW_Y_PRESSED) * lean;\n    const shadowBlur = SHADOW_BLUR - (SHADOW_BLUR - SHADOW_BLUR_PRESSED) * lean;\n    return [\n      {\n        id: `${cap.chord}-${cap.index}`,\n        chord: cap.chord,\n        index: cap.index,\n        key,\n        x: round((left + cap.x) * size),\n        y: round((1 - getKeystrokeRise(time - enter)) * metrics.rise),\n        width: round(cap.width * size),\n        opacity: round(fadeIn(time - enter) * (1 - (1 - DIM_OPACITY) * dim)),\n        depth: round(depth),\n        lift: round(depth * metrics.travel),\n        wall: round(metrics.wall - depth * metrics.travel),\n        shadowY: round(shadowY * size),\n        shadowBlur: round(shadowBlur * size),\n        color: legendColor(palette.ink, accent, clamp(depth, 0, 1)),\n      },\n    ];\n  });\n  const separators = group.layout.separators.flatMap((separator) => {\n    const enter = group.timing[separator.chord].enter;\n    if (time <= enter) return [];\n    return [\n      {\n        id: `then-${separator.chord}`,\n        chord: separator.chord,\n        x: round((left + separator.x) * size),\n        y: round((1 - getKeystrokeRise(time - enter)) * metrics.rise),\n        width: round(separator.width * size),\n        opacity: round(fadeIn(time - enter)),\n      },\n    ];\n  });\n  return {\n    id: `${group.step}`,\n    index: group.index,\n    step: group.step,\n    keys: group.keys,\n    label: group.label,\n    slot: round(slot),\n    width: round(visible * size),\n    y: round(-slot * SLOT_RISE * size + drift),\n    scale: round((1 - (1 - SLOT_SCALE) * slot) * (1 - EXIT_SCALE * leaving)),\n    opacity: round(dimmed * (1 - leaving ** 2)),\n    caps,\n    separators,\n  };\n}\n\nexport function getKeystrokeState(\n  frame: number,\n  options: KeystrokeOptions = {},\n  timeline: KeystrokeTimeline = getKeystrokeTimeline(options),\n) {\n  const width = Math.max(1, finite(options.width, 1280));\n  const height = Math.max(1, finite(options.height, 720));\n  const unit = height / 720;\n  const reference = within(\n    options.size,\n    keystrokeDefaults.size,\n    MIN_SIZE,\n    MAX_SIZE,\n  );\n  const metrics = getKeystrokeMetrics(reference * unit, unit);\n  const time = getKeystrokeTime(frame, options);\n  const x = finite(options.x, keystrokeDefaults.x);\n  const y = finite(options.y, keystrokeDefaults.y);\n  const theme = themeOf(options.theme);\n  const palette = keystrokePalettes[theme];\n  const accent = accentOf(options.accent);\n  const groups = timeline.groups\n    .filter((group) => time > group.enter && time < group.end)\n    .map((group) => groupState(group, time, metrics, palette, accent));\n  return {\n    width,\n    height,\n    unit,\n    time: round(time),\n    x,\n    y,\n    originX: round(x * width),\n    originY: round(y * height),\n    theme,\n    palette,\n    accent,\n    metrics,\n    groups,\n  };\n}\n\nexport type KeystrokeState = ReturnType<typeof getKeystrokeState>;\n\nfunction Legend({\n  legend,\n  metrics,\n}: {\n  legend: KeystrokeLegend;\n  metrics: KeystrokeMetrics;\n}) {\n  if (legend.kind === \"icon\") {\n    const icon = ICONS[legend.icon];\n    return (\n      <svg\n        width={metrics.icon}\n        height={metrics.icon}\n        viewBox={icon.box}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        aria-hidden=\"true\"\n        style={{ display: \"block\" }}\n      >\n        {icon.paths.map((d) => (\n          <path key={d} d={d} />\n        ))}\n      </svg>\n    );\n  }\n  const fontSize = legend.kind === \"word\" ? metrics.wordFont : metrics.charFont;\n  return (\n    <span\n      style={{ fontSize, fontWeight: 500, lineHeight: 1, whiteSpace: \"nowrap\" }}\n    >\n      {legend.text}\n    </span>\n  );\n}\n\nexport function Keystroke({\n  steps = keystrokeDefaultSteps,\n  platform = keystrokeDefaults.platform,\n  theme = keystrokeDefaults.theme,\n  accent = keystrokeDefaults.accent,\n  x = keystrokeDefaults.x,\n  y = keystrokeDefaults.y,\n  size = keystrokeDefaults.size,\n  linger = keystrokeDefaults.linger,\n  speed = keystrokeDefaults.speed,\n  className,\n}: KeystrokeProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const timeline = useMemo(\n    () => getKeystrokeTimeline({ steps, platform, linger, fps }),\n    [steps, platform, linger, fps],\n  );\n  const state = getKeystrokeState(\n    frame,\n    { width, height, fps, theme, accent, x, y, size, speed },\n    timeline,\n  );\n  const { metrics, palette } = state;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        pointerEvents: \"none\",\n        fontFamily: FONT_FAMILY,\n      }}\n    >\n      {state.groups.map((group) => (\n        <div\n          key={group.id}\n          style={{\n            position: \"absolute\",\n            left: `${state.x * 100}%`,\n            top: `${state.y * 100}%`,\n            width: 0,\n            height: 0,\n            opacity: group.opacity,\n            transform: `translateY(${group.y}px) scale(${group.scale})`,\n            transformOrigin: \"0 0\",\n          }}\n        >\n          {group.separators.map((separator) => (\n            <div\n              key={separator.id}\n              style={{\n                position: \"absolute\",\n                left: separator.x,\n                top: separator.y - metrics.height / 2,\n                width: separator.width,\n                height: metrics.keytop,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                opacity: separator.opacity,\n                color: palette.separator,\n                fontSize: metrics.separatorFont,\n                fontWeight: 500,\n                lineHeight: 1,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              then\n            </div>\n          ))}\n          {group.caps.map((cap) => (\n            <div\n              key={cap.id}\n              style={{\n                position: \"absolute\",\n                left: cap.x,\n                top: cap.y - metrics.height / 2,\n                width: cap.width,\n                height: metrics.height,\n                opacity: cap.opacity,\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  top: metrics.travel,\n                  bottom: 0,\n                  borderRadius: metrics.radius,\n                  background: palette.wall,\n                  boxShadow: `0 ${cap.shadowY}px ${cap.shadowBlur}px ${palette.shadow}`,\n                }}\n              />\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  top: 0,\n                  height: metrics.keytop,\n                  boxSizing: \"border-box\",\n                  borderRadius: metrics.radius,\n                  border: `${metrics.border}px solid ${palette.edge}`,\n                  background: palette.cap,\n                  color: cap.color,\n                  transform: `translateY(${cap.lift}px)`,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                }}\n              >\n                <Legend legend={cap.key.legend} metrics={metrics} />\n              </div>\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/keystroke.tsx"
    }
  ],
  "type": "registry:component"
}
