{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-walkthrough",
  "type": "registry:ui",
  "title": "Code Walkthrough",
  "description": "A presentation-style code walkthrough: each step highlights a line range, lines grow and collapse between steps, and captions explain.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/code-walkthrough.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nexport type CodeTokenType = \"keyword\" | \"string\" | \"comment\" | \"number\" | \"tag\" | \"attr\" | \"fn\" | \"type\" | \"punct\" | \"plain\";\n\nexport interface CodeToken {\n  text: string;\n  type?: CodeTokenType;\n}\n\n/** A line of code: a string (tokenized for you), or pre-tokenized, optionally with a stable key. */\nexport type CodeLine = string | { key?: string; text?: string; tokens?: CodeToken[] };\n\nexport interface CodeWalkthroughStep {\n  /** Short step title, shown above the caption and on the step dots. */\n  title?: string;\n  /** What this step explains. */\n  caption: React.ReactNode;\n  /** The whole file at this step: one string, or an array of lines. */\n  code: string | CodeLine[];\n  /** 1-based lines to emphasise (\"1-3, 7\" or [1, 2, 3, 7]); the rest dims. Omit to show every line plainly. */\n  highlight?: string | number[];\n}\n\nexport interface CodeWalkthroughProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  steps: CodeWalkthroughStep[];\n  /** Accessible name of the walkthrough. */\n  label?: string;\n  /** File name in the code panel header. */\n  fileName?: string;\n  /** Captions beside the code (stacking below it in narrow containers), always below, or not rendered (\"none\" — e.g. when you show captions elsewhere via onStepChange). */\n  captionPosition?: \"right\" | \"bottom\" | \"none\";\n  showLineNumbers?: boolean;\n  /** Opacity of lines outside the highlighted range. */\n  dimOpacity?: number;\n  /** Advance automatically (with a pause button). */\n  autoplay?: boolean;\n  /** Time per step while playing, in ms. */\n  interval?: number;\n  /** Wrap from the last step to the first (buttons, keys and autoplay). */\n  loop?: boolean;\n  /** Controlled step (0-based). */\n  step?: number;\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  accent?: string;\n}\n\n/* -------------------------------- tokenizer -------------------------------- */\n\nconst KEYWORDS = new Set(\n  \"as async await break case catch class const continue default delete do else enum export extends false finally for from function if implements import in instanceof interface let new null of private public readonly return satisfies static super switch this throw true try type typeof undefined var void while yield\".split(\n    \" \",\n  ),\n);\nconst TYPE_WORDS = new Set([\"string\", \"number\", \"boolean\", \"unknown\", \"never\", \"any\", \"object\", \"bigint\", \"symbol\"]);\n\ninterface LexState {\n  block: boolean; // inside /* */\n  template: boolean; // inside a `template` that spans lines\n  inTag: boolean; // between < and > of a JSX tag\n  closing: boolean; // the current tag is </…>\n  tagBraces: number; // {…} depth inside the tag (attribute expressions)\n  jsx: number; // open JSX element depth\n  childBraces: number; // {…} depth inside JSX children\n}\n\n/** A deliberately tiny TS/TSX tokenizer: keywords, strings, comments, numbers, JSX tags and attributes. */\nfunction tokenizeLine(line: string, state: LexState): CodeToken[] {\n  const out: CodeToken[] = [];\n  const push = (text: string, type: CodeTokenType) => {\n    if (!text) return;\n    const last = out[out.length - 1];\n    if (last && last.type === type) last.text += text;\n    else out.push({ text, type });\n  };\n  const n = line.length;\n  let i = 0;\n\n  if (state.block) {\n    const end = line.indexOf(\"*/\");\n    if (end === -1) return [{ text: line, type: \"comment\" }];\n    push(line.slice(0, end + 2), \"comment\");\n    i = end + 2;\n    state.block = false;\n  } else if (state.template) {\n    let j = 0;\n    while (j < n && line[j] !== \"`\") j += line[j] === \"\\\\\" ? 2 : 1;\n    if (j >= n) return [{ text: line, type: \"string\" }];\n    push(line.slice(0, j + 1), \"string\");\n    i = j + 1;\n    state.template = false;\n  }\n\n  /** Whether a \"<\" here opens JSX (rather than a comparison or a generic). */\n  const jsxCanStart = () => {\n    for (let k = out.length - 1; k >= 0; k--) {\n      const t = out[k];\n      if (!t.text.trim()) continue;\n      if (t.type === \"keyword\") return true;\n      return t.type === \"punct\" && /[(=,?:&|{[>};]$/.test(t.text.trimEnd());\n    }\n    return true;\n  };\n\n  while (i < n) {\n    const ch = line[i];\n    const rest = line.slice(i);\n\n    // JSX children: plain text until a tag or an {expression}.\n    if (!state.inTag && state.jsx > 0 && state.childBraces === 0 && ch !== \"<\") {\n      if (ch === \"{\") {\n        state.childBraces++;\n        push(\"{\", \"punct\");\n        i++;\n        continue;\n      }\n      const m = /^[^<{]+/.exec(rest);\n      push(m ? m[0] : ch, \"plain\");\n      i += m ? m[0].length : 1;\n      continue;\n    }\n    if (rest.startsWith(\"//\")) {\n      push(rest, \"comment\");\n      break;\n    }\n    if (rest.startsWith(\"/*\")) {\n      const end = line.indexOf(\"*/\", i + 2);\n      if (end === -1) {\n        push(rest, \"comment\");\n        state.block = true;\n        break;\n      }\n      push(line.slice(i, end + 2), \"comment\");\n      i = end + 2;\n      continue;\n    }\n    if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n      let j = i + 1;\n      while (j < n && line[j] !== ch) j += line[j] === \"\\\\\" ? 2 : 1;\n      if (j >= n && ch === \"`\") state.template = true;\n      push(line.slice(i, j + 1), \"string\");\n      i = j + 1;\n      continue;\n    }\n    if (/\\d/.test(ch) && !/[\\w$]/.test(line[i - 1] ?? \"\")) {\n      const m = /^(?:0x[\\da-f_]+|\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?n?)/i.exec(rest);\n      const num = m ? m[0] : ch;\n      push(num, \"number\");\n      i += num.length;\n      continue;\n    }\n    if (rest.startsWith(\"=>\")) {\n      push(\"=>\", \"punct\");\n      i += 2;\n      continue;\n    }\n    if (ch === \"<\" && !state.inTag) {\n      const m = /^<(\\/?)([A-Za-z][\\w.]*)?/.exec(rest);\n      const isFragment = rest.startsWith(\"<>\") || rest.startsWith(\"</>\");\n      if (m && (m[2] || isFragment) && jsxCanStart()) {\n        state.inTag = true;\n        state.closing = m[1] === \"/\";\n        state.tagBraces = 0;\n        push(state.closing ? \"</\" : \"<\", \"punct\");\n        i += state.closing ? 2 : 1;\n        if (m[2]) {\n          push(m[2], \"tag\");\n          i += m[2].length;\n        }\n        continue;\n      }\n    }\n    if (state.inTag && state.tagBraces === 0) {\n      if (rest.startsWith(\"/>\")) {\n        push(\"/>\", \"punct\");\n        state.inTag = false;\n        i += 2;\n        continue;\n      }\n      if (ch === \">\") {\n        push(\">\", \"punct\");\n        state.inTag = false;\n        state.jsx = Math.max(0, state.jsx + (state.closing ? -1 : 1));\n        i++;\n        continue;\n      }\n    }\n    const id = /^[A-Za-z_$][\\w$]*/.exec(rest);\n    if (id) {\n      const word = id[0];\n      const after = line.slice(i + word.length);\n      let type: CodeTokenType = \"plain\";\n      if (state.inTag && state.tagBraces === 0 && /^\\s*=/.test(after)) type = \"attr\";\n      else if (KEYWORDS.has(word)) type = \"keyword\";\n      else if (TYPE_WORDS.has(word) || /^[A-Z]/.test(word)) type = \"type\";\n      else if (/^\\s*(?:<[\\w\\s,[\\]]*>)?\\s*\\(/.test(after)) type = \"fn\";\n      push(word, type);\n      i += word.length;\n      continue;\n    }\n    if (/\\s/.test(ch)) {\n      const m = /^\\s+/.exec(rest);\n      push(m ? m[0] : ch, \"plain\");\n      i += m ? m[0].length : 1;\n      continue;\n    }\n    // Brace depth decides whether \">\" ends a tag and where JSX children resume.\n    if (ch === \"{\") {\n      if (state.inTag) state.tagBraces++;\n      else if (state.jsx > 0) state.childBraces++;\n    } else if (ch === \"}\") {\n      if (state.inTag) state.tagBraces = Math.max(0, state.tagBraces - 1);\n      else if (state.jsx > 0) state.childBraces = Math.max(0, state.childBraces - 1);\n    }\n    push(ch, \"punct\");\n    i++;\n  }\n  return out;\n}\n\nfunction tokenizeAll(lines: string[]): CodeToken[][] {\n  const state: LexState = { block: false, template: false, inTag: false, closing: false, tagBraces: 0, jsx: 0, childBraces: 0 };\n  return lines.map((line) => tokenizeLine(line, state));\n}\n\n/* ------------------------------ steps → lines ------------------------------ */\n\ninterface ResolvedLine {\n  key: string;\n  text: string;\n  tokens: CodeToken[];\n  /** New in this step (its key didn't exist in the previous one). */\n  added: boolean;\n}\n\nfunction parseHighlight(spec: string | number[] | undefined): Set<number> | null {\n  if (spec === undefined) return null;\n  if (Array.isArray(spec)) return new Set(spec);\n  const lines = new Set<number>();\n  for (const part of spec.split(\",\")) {\n    const [a, b] = part.split(\"-\").map((s) => Number.parseInt(s.trim(), 10));\n    if (Number.isNaN(a)) continue;\n    for (let n = a; n <= (Number.isNaN(b ?? Number.NaN) ? a : b); n++) lines.add(n);\n  }\n  return lines;\n}\n\n/**\n * Weighted longest common subsequence of two line lists, as index pairs. Lines weigh by their\n * length, so a real line of code always wins over a stray blank line or a lone brace.\n */\nfunction lcsPairs(a: string[], b: string[]): [number, number][] {\n  const weight = (line: string) => line.trim().length * 2 + 1;\n  const table = Array.from({ length: a.length + 1 }, () => new Float64Array(b.length + 1));\n  for (let i = a.length - 1; i >= 0; i--) {\n    for (let j = b.length - 1; j >= 0; j--) {\n      table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + weight(a[i]) : Math.max(table[i + 1][j], table[i][j + 1]);\n    }\n  }\n  const pairs: [number, number][] = [];\n  for (let i = 0, j = 0; i < a.length && j < b.length; ) {\n    if (a[i] === b[j] && table[i][j] === table[i + 1][j + 1] + weight(a[i])) {\n      pairs.push([i, j]);\n      i++;\n      j++;\n    } else if (table[i + 1][j] >= table[i][j + 1]) i++;\n    else j++;\n  }\n  return pairs;\n}\n\n/**\n * Gives every line a key that survives between steps: explicit keys win; otherwise unchanged lines\n * (matched by an LCS on their text) keep the previous step's key, so only real edits animate.\n */\nfunction resolveSteps(steps: CodeWalkthroughStep[]): ResolvedLine[][] {\n  let counter = 0;\n  let previous: ResolvedLine[] = [];\n  return steps.map((step) => {\n    const raw: CodeLine[] = typeof step.code === \"string\" ? step.code.replace(/\\r\\n/g, \"\\n\").replace(/\\n$/, \"\").split(\"\\n\") : step.code;\n    const texts = raw.map((l) => (typeof l === \"string\" ? l : (l.text ?? (l.tokens ?? []).map((t) => t.text).join(\"\"))));\n    const auto = tokenizeAll(texts);\n    const keys: (string | null)[] = raw.map((l) => (typeof l === \"string\" ? null : (l.key ?? null)));\n    for (const [pi, ci] of lcsPairs(\n      previous.map((l) => l.text),\n      texts,\n    )) {\n      if (keys[ci] === null && !keys.includes(previous[pi].key)) keys[ci] = previous[pi].key;\n    }\n    const prevKeys = new Set(previous.map((l) => l.key));\n    const lines = raw.map((l, i) => {\n      const key = keys[i] ?? `l${counter++}`;\n      const tokens = typeof l !== \"string\" && l.tokens ? l.tokens : auto[i];\n      return { key, text: texts[i], tokens, added: previous.length > 0 && !prevKeys.has(key) && texts[i].trim() !== \"\" };\n    });\n    previous = lines;\n    return lines;\n  });\n}\n\n/* -------------------------------- component -------------------------------- */\n\nconst TOKEN_CLASS: Record<CodeTokenType, string> = {\n  keyword: \"text-[#C2410C] dark:text-[#FF9470]\",\n  string: \"text-[#15803D] dark:text-[#9FE6BC]\",\n  comment: \"italic text-zinc-400 dark:text-zinc-500\",\n  number: \"text-[#1D4ED8] dark:text-[#93C5FD]\",\n  tag: \"text-[#0E7490] dark:text-[#67E8F9]\",\n  attr: \"text-[#A16207] dark:text-[#FCD34D]\",\n  fn: \"text-zinc-950 dark:text-white\",\n  type: \"text-[#0E7490] dark:text-[#67E8F9]\",\n  punct: \"text-zinc-500 dark:text-zinc-400\",\n  plain: \"text-zinc-800 dark:text-zinc-200\",\n};\n\nfunction subscribeReducedMotion(cb: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n}\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/**\n * A presentation-style code walkthrough. Each step shows the file as it stands, emphasises a line\n * range (the rest dims) and explains it in a caption. Lines added or removed between steps grow and\n * collapse in place, matched by stable line keys so unchanged lines never jump. Navigate with the\n * buttons, step dots, Left / Right / Home / End while focused, or let it autoplay with a pause.\n * Syntax colours come from pre-tokenized lines or a tiny built-in TSX tokenizer, in both themes.\n */\nexport function CodeWalkthrough({\n  steps,\n  label = \"Code walkthrough\",\n  fileName = \"example.tsx\",\n  captionPosition = \"right\",\n  showLineNumbers = true,\n  dimOpacity = 0.35,\n  autoplay = false,\n  interval = 4000,\n  loop = false,\n  step: stepProp,\n  defaultStep = 0,\n  onStepChange,\n  accent = \"#FF7448\",\n  className = \"\",\n  style,\n  onKeyDown,\n  ...rest\n}: CodeWalkthroughProps) {\n  const reduced = usePrefersReducedMotion();\n  const resolved = React.useMemo(() => resolveSteps(steps), [steps]);\n  const count = steps.length;\n  const [internalStep, setInternalStep] = React.useState(defaultStep);\n  const step = Math.min(Math.max(0, stepProp ?? internalStep), Math.max(0, count - 1));\n  const [playing, setPlaying] = React.useState(autoplay);\n  const [prevAutoplay, setPrevAutoplay] = React.useState(autoplay);\n  if (prevAutoplay !== autoplay) {\n    setPrevAutoplay(autoplay);\n    setPlaying(autoplay);\n  }\n  const [onScreen, setOnScreen] = React.useState(true);\n  const [pageVisible, setPageVisible] = React.useState(true);\n  const rootRef = React.useRef<HTMLElement>(null);\n  const uid = React.useId();\n\n  const goTo = (next: number) => {\n    const target = loop ? (next + count) % count : Math.min(Math.max(0, next), count - 1);\n    if (target === step || count === 0) return;\n    if (stepProp === undefined) setInternalStep(target);\n    onStepChange?.(target);\n  };\n\n  // Autoplay pauses while off screen or in a hidden tab, and stops at the end unless looping.\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([entry]) => setOnScreen(entry.isIntersecting), { threshold: 0.25 });\n    io.observe(el);\n    const onVisibility = () => setPageVisible(!document.hidden);\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    return () => {\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n  }, []);\n  const running = playing && onScreen && pageVisible && count > 1;\n  // The latest \"advance\" closure, so the timer effect only restarts when the step or timing changes.\n  const advance = React.useRef(() => {});\n  React.useEffect(() => {\n    advance.current = () => {\n      if (step >= count - 1 && !loop) {\n        // Without looping, autoplay starts over from the first step.\n        if (stepProp === undefined) setInternalStep(0);\n        onStepChange?.(0);\n      } else goTo(step + 1);\n    };\n  });\n  React.useEffect(() => {\n    if (!running) return;\n    const id = window.setTimeout(() => advance.current(), interval);\n    return () => window.clearTimeout(id);\n  }, [running, step, interval]);\n\n  const lines = resolved[step] ?? [];\n  // Reserve room for the longest step so the controls don't jump as lines come and go.\n  const maxLines = resolved.reduce((m, l) => Math.max(m, l.length), 0);\n  const highlight = parseHighlight(steps[step]?.highlight);\n  const current = steps[step];\n  const spring = reduced ? { duration: 0 } : { type: \"spring\" as const, stiffness: 380, damping: 36, mass: 0.8 };\n  const atStart = !loop && step === 0;\n  const atEnd = !loop && step === count - 1;\n\n  const button =\n    \"grid size-8 place-items-center rounded-lg border border-zinc-900/10 text-zinc-600 outline-hidden transition-colors hover:bg-zinc-900/[0.05] hover:text-zinc-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cw-accent)] disabled:pointer-events-none disabled:opacity-35 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/[0.07] dark:hover:text-white\";\n\n  const caption = (\n    <div className=\"min-w-0\" aria-live=\"polite\" aria-atomic=\"true\">\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.div\n          key={step}\n          initial={reduced ? { opacity: 1 } : { opacity: 0, y: 6 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={reduced ? { opacity: 1 } : { opacity: 0, y: -4 }}\n          transition={reduced ? { duration: 0 } : { duration: 0.22, ease: [0.22, 1, 0.36, 1] }}\n        >\n          <p className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-[color-mix(in_srgb,var(--cw-accent)_75%,#52525b)] dark:text-[var(--cw-accent)]\">\n            Step {String(step + 1).padStart(2, \"0\")} / {String(count).padStart(2, \"0\")}\n          </p>\n          {current?.title ? <p className=\"mt-2 text-[17px] font-semibold tracking-[-0.02em] text-zinc-950 dark:text-white\">{current.title}</p> : null}\n          <div className=\"mt-2 text-[14px] leading-relaxed text-zinc-600 dark:text-zinc-400\">{current?.caption}</div>\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  );\n\n  return (\n    <section\n      ref={rootRef}\n      aria-roledescription=\"code walkthrough\"\n      aria-label={label}\n      {...rest}\n      className={`@container w-full min-w-0 ${className}`}\n      style={{ ...style, [\"--cw-accent\" as string]: accent }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n        const map: Record<string, number> = { ArrowRight: step + 1, ArrowLeft: step - 1, Home: 0, End: count - 1 };\n        if (!(event.key in map)) return;\n        event.preventDefault();\n        setPlaying(false);\n        goTo(map[event.key]);\n      }}\n    >\n      <div className={`grid grid-cols-1 gap-5 ${captionPosition === \"right\" ? \"@3xl:grid-cols-[minmax(0,1fr)_minmax(220px,280px)] @3xl:items-center\" : \"\"}`}>\n        <div className=\"min-w-0 overflow-hidden rounded-xl border border-zinc-900/10 bg-[#FBFAF7] dark:border-white/10 dark:bg-[#111316]\">\n          <div className=\"flex items-center gap-3 border-b border-zinc-900/10 px-4 py-2.5 dark:border-white/10\">\n            <span aria-hidden=\"true\" className=\"flex gap-1.5\">\n              <span className=\"size-2 rounded-full bg-zinc-900/15 dark:bg-white/15\" />\n              <span className=\"size-2 rounded-full bg-zinc-900/15 dark:bg-white/15\" />\n              <span className=\"size-2 rounded-full bg-zinc-900/15 dark:bg-white/15\" />\n            </span>\n            <span className=\"truncate font-mono text-[11px] text-zinc-500 dark:text-zinc-400\">{fileName}</span>\n            <span className=\"ml-auto shrink-0 font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-400 dark:text-zinc-500\">\n              {lines.length} lines\n            </span>\n          </div>\n          <pre\n            tabIndex={0}\n            role=\"region\"\n            aria-label={`${fileName}, step ${step + 1} of ${count}. Use the left and right arrow keys to change step.`}\n            aria-describedby={`${uid}-hint`}\n            style={{ minHeight: `calc(${maxLines} * 1.7em + 24px)` }}\n            className=\"overflow-x-auto py-3 font-mono text-[12.5px] leading-[1.7] outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cw-accent)]\"\n          >\n            <code className=\"block w-max min-w-full\">\n              <AnimatePresence initial={false}>\n                {lines.map((line, index) => {\n                  const lit = highlight === null || highlight.has(index + 1);\n                  const emphasised = highlight !== null && lit;\n                  return (\n                    <motion.span\n                      key={line.key}\n                      initial={{ height: 0, opacity: 0 }}\n                      animate={{ height: \"auto\", opacity: lit ? 1 : dimOpacity }}\n                      exit={{ height: 0, opacity: 0 }}\n                      transition={spring}\n                      className=\"block overflow-hidden\"\n                    >\n                      <span\n                        className={`relative flex pr-5 transition-colors duration-300 ${emphasised ? \"bg-[color-mix(in_srgb,var(--cw-accent)_9%,transparent)]\" : \"\"}`}\n                      >\n                        <span\n                          aria-hidden=\"true\"\n                          className={`absolute inset-y-0 left-0 w-[2px] transition-opacity duration-300 ${emphasised ? \"bg-[var(--cw-accent)] opacity-100\" : \"opacity-0\"}`}\n                        />\n                        {showLineNumbers ? (\n                          <span aria-hidden=\"true\" className=\"w-11 shrink-0 select-none pr-3 text-right tabular-nums text-zinc-400 dark:text-zinc-600\">\n                            {index + 1}\n                          </span>\n                        ) : (\n                          <span aria-hidden=\"true\" className=\"w-4 shrink-0\" />\n                        )}\n                        <span aria-hidden=\"true\" className=\"w-4 shrink-0 select-none text-[var(--cw-accent)]\">\n                          {line.added ? \"+\" : \"\"}\n                        </span>\n                        <span className=\"whitespace-pre\">\n                          {line.tokens.length\n                            ? line.tokens.map((t, k) => (\n                                <span key={k} className={TOKEN_CLASS[t.type ?? \"plain\"] ?? TOKEN_CLASS.plain}>\n                                  {t.text}\n                                </span>\n                              ))\n                            : \"​\"}\n                        </span>\n                      </span>\n                    </motion.span>\n                  );\n                })}\n              </AnimatePresence>\n            </code>\n          </pre>\n        </div>\n        {captionPosition === \"right\" ? caption : null}\n      </div>\n      {captionPosition === \"bottom\" ? <div className=\"mt-5\">{caption}</div> : null}\n\n      <div className=\"mt-5 flex items-center gap-3\">\n        <button type=\"button\" aria-label=\"Previous step\" disabled={atStart} className={button} onClick={() => { setPlaying(false); goTo(step - 1); }}>\n          <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <path d=\"m10 4-4 4 4 4\" />\n          </svg>\n        </button>\n        <div role=\"group\" aria-label=\"Steps\" className=\"flex flex-1 items-center gap-1.5\">\n          {steps.map((s, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              aria-label={`Step ${i + 1}${s.title ? `: ${s.title}` : \"\"}`}\n              aria-current={i === step ? \"step\" : undefined}\n              onClick={() => {\n                setPlaying(false);\n                goTo(i);\n              }}\n              className=\"group relative grid h-6 min-w-6 flex-1 place-items-center rounded-md outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-[var(--cw-accent)]\"\n            >\n              <span className=\"relative block h-[3px] w-full overflow-hidden rounded-full bg-zinc-900/10 transition-colors group-hover:bg-zinc-900/20 dark:bg-white/10 dark:group-hover:bg-white/20\">\n                <span\n                  key={i === step && running ? `run-${step}` : \"idle\"}\n                  className=\"absolute inset-y-0 left-0 rounded-full bg-[var(--cw-accent)]\"\n                  style={\n                    i < step || (i === step && !running)\n                      ? { width: \"100%\" }\n                      : i === step && running && !reduced\n                        ? { width: \"100%\", animation: `${uid.replace(/[^a-zA-Z0-9_-]/g, \"\")}-fill ${interval}ms linear both` }\n                        : { width: i === step ? \"100%\" : \"0%\" }\n                  }\n                />\n              </span>\n            </button>\n          ))}\n        </div>\n        <button type=\"button\" aria-label=\"Next step\" disabled={atEnd} className={button} onClick={() => { setPlaying(false); goTo(step + 1); }}>\n          <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <path d=\"m6 4 4 4-4 4\" />\n          </svg>\n        </button>\n        {autoplay ? (\n          <button type=\"button\" aria-label={playing ? \"Pause autoplay\" : \"Play autoplay\"} aria-pressed={playing} className={button} onClick={() => setPlaying((p) => !p)}>\n            {playing ? (\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"currentColor\">\n                <rect x=\"4\" y=\"3.5\" width=\"2.6\" height=\"9\" rx=\"0.8\" />\n                <rect x=\"9.4\" y=\"3.5\" width=\"2.6\" height=\"9\" rx=\"0.8\" />\n              </svg>\n            ) : (\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"currentColor\">\n                <path d=\"M5 3.6v8.8a.6.6 0 0 0 .9.5l7-4.4a.6.6 0 0 0 0-1l-7-4.4a.6.6 0 0 0-.9.5Z\" />\n              </svg>\n            )}\n          </button>\n        ) : null}\n      </div>\n      <p id={`${uid}-hint`} hidden>\n        Left and right arrow keys change step; Home and End jump to the first and last step.\n      </p>\n      {/* Keyframes for the autoplay progress bar (name scoped to this instance). */}\n      <style>{`@keyframes ${uid.replace(/[^a-zA-Z0-9_-]/g, \"\")}-fill{from{width:0%}to{width:100%}}`}</style>\n    </section>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "text"
  ],
  "docs": "Documentation: https://wancraft.my/components/code-walkthrough"
}
