{
  "name": "page-flip",
  "type": "registry:component",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Page Flip\n * @version: 1.0.0\n * @date: 2026-10-21\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { animate, useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Pt {\n  x: number;\n  y: number;\n}\n\n/** A page being turned: which way, by which corner, and where that corner is. */\ninterface Turn {\n  dir: 1 | -1;\n  /** The corner's resting point, in book pixels. */\n  corner: Pt;\n  /** Where the corner is now. */\n  at: Pt;\n}\n\nexport interface PageFlipProps {\n  /** Pages in reading order. The first is the cover, alone on the right. */\n  pages?: ReactNode[];\n  /** Called with the spread index after a page settles. */\n  onSpreadChange?: (spread: number) => void;\n  className?: string;\n}\n\n/** Below this frame width (px), the book shows one page at a time. */\nconst SINGLE_BELOW = 520;\n/** Marks a layer that isn't drawn. */\nconst HIDDEN = Symbol(\"hidden\");\n\ninterface Layers {\n  left?: ReactNode;\n  right?: ReactNode;\n  /** The page being turned, on its own side. */\n  turning?: ReactNode;\n  /** What's printed on the back of the turning page. */\n  back?: ReactNode | typeof HIDDEN;\n}\n\n/** Two pages open: the cover alone on the right, then pairs. */\nfunction twoPages(pages: ReactNode[], spread: number, dir?: 1 | -1): Layers {\n  const leftOf = (s: number) => (s > 0 ? pages[2 * s - 1] : undefined);\n  const rightOf = (s: number) => pages[2 * s];\n  if (!dir) {\n    return { left: leftOf(spread), right: rightOf(spread) };\n  }\n  // While turning, what shows under the page is the next spread's.\n  const target = spread + dir;\n  return dir === 1\n    ? {\n        left: leftOf(spread),\n        right: rightOf(target),\n        turning: rightOf(spread),\n        back: leftOf(target),\n      }\n    : {\n        left: leftOf(target),\n        right: rightOf(spread),\n        turning: leftOf(spread),\n        back: rightOf(target),\n      };\n}\n\n/**\n * One page at a time: a page turns off to the left with blank paper on its\n * back, and turning back brings the previous page in over the current one.\n */\nfunction onePage(pages: ReactNode[], page: number, dir?: 1 | -1): Layers {\n  if (dir === 1) {\n    return { right: pages[page + 1], turning: pages[page], back: null };\n  }\n  if (dir === -1) {\n    return { right: pages[page], back: pages[page - 1] ?? HIDDEN };\n  }\n  return { right: pages[page] };\n}\n\n/** How far to slide a closed book so its one page sits in the middle. */\nfunction centerShift({ left, right }: Layers) {\n  if (left === undefined) {\n    return -0.25;\n  }\n  return right === undefined ? 0.25 : 0;\n}\n\n/** Corner the pointer has to be within (px) for the page to lift on hover. */\nconst PEEK_ZONE = 64;\n/** How far a hovered corner lifts toward the pointer, at most (px). */\nconst PEEK_MAX = 34;\n/** Pointer travel that turns a press into a drag (px). */\nconst DRAG_SLOP = 4;\nconst TURN = { duration: 0.6, ease: [0.32, 0.72, 0, 1] } as const;\nconst SETTLE = { duration: 0.32, ease: [0.23, 1, 0.32, 1] } as const;\n\nconst sub = (a: Pt, b: Pt): Pt => ({ x: a.x - b.x, y: a.y - b.y });\nconst dot = (a: Pt, b: Pt) => a.x * b.x + a.y * b.y;\nconst len = (a: Pt) => Math.hypot(a.x, a.y);\nconst lerp = (a: Pt, b: Pt, t: number): Pt => ({\n  x: a.x + (b.x - a.x) * t,\n  y: a.y + (b.y - a.y) * t,\n});\n\n/** Keeps a point within `radius` of `center`. */\nfunction within(p: Pt, center: Pt, radius: number): Pt {\n  const d = sub(p, center);\n  const l = len(d);\n  if (l <= radius) {\n    return p;\n  }\n  return { x: center.x + (d.x / l) * radius, y: center.y + (d.y / l) * radius };\n}\n\n/** The part of a polygon on one side of a line through `m` with normal `n`. */\nfunction clipPolygon(poly: Pt[], m: Pt, n: Pt, keepNegative: boolean): Pt[] {\n  const side = (p: Pt) => {\n    const d = dot(sub(p, m), n);\n    return keepNegative ? -d : d;\n  };\n  const out: Pt[] = [];\n  for (let i = 0; i < poly.length; i++) {\n    const a = poly[i];\n    const b = poly[(i + 1) % poly.length];\n    const da = side(a);\n    const db = side(b);\n    if (da >= 0) {\n      out.push(a);\n    }\n    if (da >= 0 !== db >= 0) {\n      out.push(lerp(a, b, da / (da - db)));\n    }\n  }\n  return out;\n}\n\nconst polygon = (points: Pt[], dx: number) =>\n  points.length\n    ? `polygon(${points.map((p) => `${(p.x - dx).toFixed(2)}px ${p.y.toFixed(2)}px`).join(\", \")})`\n    : \"polygon(0 0)\";\n\n/**\n * Where everything goes for a corner at `at`: the fold is the line halfway\n * between the corner's rest and where it is now. The page keeps the spine\n * side, and the flap (its back) is mirrored across the fold.\n */\nfunction foldStyles(turn: Turn, w: number, h: number) {\n  const x0 = turn.dir === 1 ? w : 0;\n  const page = [\n    { x: x0, y: 0 },\n    { x: x0 + w, y: 0 },\n    { x: x0 + w, y: h },\n    { x: x0, y: h },\n  ];\n  const d = sub(turn.at, turn.corner);\n  const l = len(d);\n  if (l < 0.5) {\n    return null;\n  }\n  const n = { x: d.x / l, y: d.y / l };\n  const m = lerp(turn.corner, turn.at, 0.5);\n  const front = clipPolygon(page, m, n, false);\n  const flap = clipPolygon(page, m, n, true);\n  // Reflection across the fold: X -> A X + b.\n  const a = 1 - 2 * n.x * n.x;\n  const bc = -2 * n.x * n.y;\n  const dd = 1 - 2 * n.y * n.y;\n  const k = 2 * dot(m, n);\n  const bx = k * n.x;\n  const by = k * n.y;\n  // The flap element sits at the page's spot; CSS moves it from there.\n  const e = a * x0 + bx - x0;\n  const f = bc * x0 + by;\n  return {\n    front: { clipPath: polygon(front, x0) } satisfies CSSProperties,\n    flap: {\n      clipPath: polygon(flap, x0),\n      transform: `matrix(${a}, ${bc}, ${bc}, ${dd}, ${e}, ${f})`,\n    } satisfies CSSProperties,\n    /** How far through the turn, for the shadow. */\n    progress: Math.min(1, l / (2 * w)),\n  };\n}\n\nexport default function PageFlip({\n  pages = DEFAULT_PAGES,\n  onSpreadChange,\n  className,\n}: PageFlipProps) {\n  const reduceMotion = useReducedMotion();\n  const bookRef = useRef<HTMLDivElement>(null);\n  const [size, setSize] = useState({ w: 0, h: 0 });\n  const frameRef = useRef<HTMLDivElement>(null);\n  /** Narrow frames show one page at a time; the spine sits off to the left. */\n  const [single, setSingle] = useState(false);\n  const singleRef = useRef(false);\n  // The spread on two pages, or the page on one.\n  const [spread, setSpread] = useState(0);\n  const [turn, setTurn] = useState<Turn | null>(null);\n  /** The spread a committed turn is heading for, so the book recenters with it. */\n  const [heading, setHeading] = useState<number | null>(null);\n  const turnRef = useRef<Turn | null>(null);\n  const pressRef = useRef<{\n    start: Pt;\n    dragging: boolean;\n    back?: boolean;\n  } | null>(null);\n  const animRef = useRef<ReturnType<typeof animate> | null>(null);\n  const playingRef = useRef(false);\n  /** A turn that will finish on its own: presses wait for it. */\n  const committingRef = useRef(false);\n  const onSpreadChangeRef = useRef(onSpreadChange);\n  onSpreadChangeRef.current = onSpreadChange;\n\n  const spreads = single ? pages.length : Math.floor(pages.length / 2) + 1;\n\n  useLayoutEffect(() => {\n    const book = bookRef.current;\n    const frame = frameRef.current;\n    if (!(book && frame)) {\n      return;\n    }\n    const measure = () => {\n      const narrow = frame.clientWidth < SINGLE_BELOW;\n      if (narrow !== singleRef.current) {\n        singleRef.current = narrow;\n        setSingle(narrow);\n        // Keep the same page in view when the layout changes.\n        setSpread((s) => (narrow ? Math.max(0, 2 * s - 1) : Math.ceil(s / 2)));\n      }\n      setSize({ w: book.clientWidth / 2, h: book.clientHeight });\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(frame);\n    observer.observe(book);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => () => animRef.current?.stop(), []);\n\n  const stop = () => {\n    animRef.current?.stop();\n    playingRef.current = false;\n  };\n\n  const setAt = useCallback((next: Turn | null) => {\n    turnRef.current = next;\n    setTurn(next);\n  }, []);\n\n  /** Keeps the corner where paper can reach: the page stays on its spine. */\n  const reach = useCallback(\n    (p: Pt, corner: Pt) => {\n      const { w, h } = size;\n      const spineSame = { x: w, y: corner.y };\n      const spineOther = { x: w, y: corner.y === 0 ? h : 0 };\n      return within(within(p, spineSame, w), spineOther, Math.hypot(w, h));\n    },\n    [size]\n  );\n\n  const cornerFor = (dir: 1 | -1, y: number): Pt => ({\n    x: dir === 1 ? 2 * size.w : 0,\n    y: y < size.h / 2 ? 0 : size.h,\n  });\n\n  /** Runs the corner from where it is to `to`, lifting it on the way over. */\n  const run = (from: Turn, to: Pt, complete: boolean) => {\n    stop();\n    const start = from.at;\n    const lift = complete ? size.h * 0.14 * (from.corner.y === 0 ? 1 : -1) : 0;\n    committingRef.current = complete;\n    const next = spread + from.dir;\n    if (complete) {\n      setHeading(next);\n    }\n    playingRef.current = true;\n    animRef.current = animate(0, 1, {\n      ...(complete ? TURN : SETTLE),\n      onUpdate: (t) => {\n        const p = lerp(start, to, t);\n        setAt({\n          ...from,\n          at: { x: p.x, y: p.y + Math.sin(Math.PI * t) * lift },\n        });\n      },\n      onComplete: () => {\n        playingRef.current = false;\n        committingRef.current = false;\n        setAt(null);\n        setHeading(null);\n        if (complete) {\n          setSpread(next);\n          onSpreadChangeRef.current?.(next);\n        }\n      },\n    });\n  };\n\n  const canTurn = (dir: 1 | -1) =>\n    dir === 1 ? spread < spreads - 1 : spread > 0;\n\n  const go = (dir: 1 | -1) => {\n    if (!(canTurn(dir) && size.w) || committingRef.current) {\n      return;\n    }\n    if (reduceMotion) {\n      setSpread(spread + dir);\n      onSpreadChangeRef.current?.(spread + dir);\n      return;\n    }\n    const corner = cornerFor(dir, size.h);\n    run(\n      { dir, corner, at: { x: corner.x - dir * 1, y: corner.y - 1 } },\n      { x: corner.x - dir * 2 * size.w, y: corner.y },\n      true\n    );\n  };\n\n  const local = (event: PointerEvent): Pt => {\n    const rect = bookRef.current?.getBoundingClientRect();\n    return rect\n      ? { x: event.clientX - rect.left, y: event.clientY - rect.top }\n      : { x: 0, y: 0 };\n  };\n\n  const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    if (event.button !== 0 || !size.w || committingRef.current) {\n      return;\n    }\n    const p = local(event);\n    // On one page, its left edge goes back and the rest turns forward.\n    const backZone = single && p.x < size.w * 1.3;\n    const dir: 1 | -1 = p.x >= size.w && !backZone ? 1 : -1;\n    if (!canTurn(dir)) {\n      return;\n    }\n    if (reduceMotion) {\n      // No corner to drag: a press turns the page in place.\n      go(dir);\n      return;\n    }\n    stop();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    pressRef.current = { start: p, dragging: false };\n    if (single && dir === -1) {\n      pressRef.current = { start: p, dragging: false, back: true };\n      return;\n    }\n    const current = turnRef.current;\n    const corner = current?.dir === dir ? current.corner : cornerFor(dir, p.y);\n    setAt({ dir, corner, at: current?.dir === dir ? current.at : corner });\n  };\n\n  /** A corner near the pointer lifts a little, inviting the turn. */\n  const peek = (p: Pt) => {\n    const dir: 1 | -1 = p.x >= size.w ? 1 : -1;\n    const corner = cornerFor(dir, p.y);\n    const near = canTurn(dir) && len(sub(p, corner)) < PEEK_ZONE;\n    const current = turnRef.current;\n    if (!near) {\n      if (current) {\n        run(current, current.corner, false);\n      }\n      return;\n    }\n    stop();\n    const toward = sub(p, corner);\n    const l = Math.max(1, len(toward));\n    const lift = Math.min(PEEK_MAX, l * 0.6 + 14);\n    setAt({\n      dir,\n      corner,\n      at: reach(\n        {\n          x: corner.x + (toward.x / l) * lift,\n          y: corner.y + (toward.y / l) * lift,\n        },\n        corner\n      ),\n    });\n  };\n\n  const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    const p = local(event);\n    const press = pressRef.current;\n    const current = turnRef.current;\n    if (press?.back) {\n      press.dragging ||= len(sub(p, press.start)) >= DRAG_SLOP;\n      return;\n    }\n    if (press && current) {\n      if (!press.dragging && len(sub(p, press.start)) < DRAG_SLOP) {\n        return;\n      }\n      press.dragging = true;\n      setAt({ ...current, at: reach(p, current.corner) });\n      return;\n    }\n    if (event.pointerType === \"mouse\" && !playingRef.current && !reduceMotion) {\n      peek(p);\n    }\n  };\n\n  const onPointerUp = () => {\n    const press = pressRef.current;\n    const current = turnRef.current;\n    pressRef.current = null;\n    if (press?.back) {\n      if (!press.dragging) {\n        go(-1);\n      }\n      return;\n    }\n    // Only a press that started on the page turns it: a release after a\n    // right click, or a press that began outside, leaves a peek to settle.\n    if (!(press && current)) {\n      return;\n    }\n    // A tap turns the page; a drag turns it once the corner crosses the spine.\n    const crossed =\n      current.dir === 1 ? current.at.x < size.w : current.at.x > size.w;\n    const complete = press.dragging ? crossed : true;\n    run(\n      current,\n      complete\n        ? {\n            x: current.corner.x - current.dir * 2 * size.w,\n            y: current.corner.y,\n          }\n        : current.corner,\n      complete\n    );\n  };\n\n  const onPointerLeave = () => {\n    const current = turnRef.current;\n    if (!pressRef.current && current && !playingRef.current) {\n      run(current, current.corner, false);\n    }\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      go(event.key === \"ArrowRight\" ? 1 : -1);\n    }\n  };\n\n  const fold = turn && size.w ? foldStyles(turn, size.w, size.h) : null;\n  const { left, right, turning, back } = single\n    ? onePage(pages, spread, turn?.dir)\n    : twoPages(pages, spread, turn?.dir);\n  // A closed book sits centered: the cover alone, or the back alone.\n  const shift = single ? 0 : centerShift(twoPages(pages, heading ?? spread));\n  const first = single ? spread + 1 : Math.max(1, 2 * spread);\n  const last = single ? spread + 1 : Math.min(pages.length, 2 * spread + 1);\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full max-w-2xl flex-col items-center overflow-x-clip\",\n        className\n      )}\n      ref={frameRef}\n    >\n      <div\n        className={cn(\n          \"shrink-0 rounded-sm motion-reduce:transition-none\",\n          single\n            ? \"w-[200%] self-start\"\n            : \"w-full transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]\"\n        )}\n        style={{ transform: `translateX(${single ? -50 : shift * 100}%)` }}\n      >\n        <div\n          className=\"relative aspect-[3/2] w-full touch-none select-none\"\n          onPointerCancel={onPointerUp}\n          onPointerDown={onPointerDown}\n          onPointerLeave={onPointerLeave}\n          onPointerMove={onPointerMove}\n          onPointerUp={onPointerUp}\n          ref={bookRef}\n        >\n          {left !== undefined && <Paper side=\"left\">{left}</Paper>}\n          {right !== undefined && <Paper side=\"right\">{right}</Paper>}\n          {turn && fold && turning !== undefined && (\n            <Paper side={turn.dir === 1 ? \"right\" : \"left\"} style={fold.front}>\n              {turning}\n            </Paper>\n          )}\n          {turn && !fold && turning !== undefined && (\n            <Paper side={turn.dir === 1 ? \"right\" : \"left\"}>{turning}</Paper>\n          )}\n          {turn && fold && back !== undefined && back !== HIDDEN && (\n            // The shadow sits on a wrapper: a clip on the same element would cut it off.\n            <div\n              className=\"pointer-events-none absolute inset-0\"\n              style={{\n                filter: `drop-shadow(0 ${2 + fold.progress * 6}px ${6 + fold.progress * 10}px rgb(0 0 0 / ${0.12 + (1 - fold.progress) * 0.1}))`,\n              }}\n            >\n              <Paper\n                back\n                side={turn.dir === 1 ? \"right\" : \"left\"}\n                style={fold.flap}\n              >\n                {back}\n              </Paper>\n            </div>\n          )}\n        </div>\n      </div>\n\n      <div className=\"mt-5 flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-400\">\n        <NavButton\n          disabled={spread === 0}\n          label=\"Previous page\"\n          onClick={() => go(-1)}\n          onKeyDown={onKeyDown}\n        >\n          <path d=\"M10 4 6 8l4 4\" />\n        </NavButton>\n        <span aria-live=\"polite\" className=\"min-w-24 text-center tabular-nums\">\n          {first === last ? `${first}` : `${first}–${last}`} of {pages.length}\n        </span>\n        <NavButton\n          disabled={spread === spreads - 1}\n          label=\"Next page\"\n          onClick={() => go(1)}\n          onKeyDown={onKeyDown}\n        >\n          <path d=\"m6 4 4 4-4 4\" />\n        </NavButton>\n      </div>\n    </div>\n  );\n}\n\nfunction Paper({\n  side,\n  back = false,\n  style,\n  children,\n}: {\n  side: \"left\" | \"right\";\n  back?: boolean;\n  style?: CSSProperties;\n  children: ReactNode;\n}) {\n  return (\n    <div\n      className={cn(\n        \"@container absolute top-0 h-full w-1/2 origin-top-left overflow-hidden text-zinc-900 dark:text-zinc-100\",\n        // The back of a sheet is a shade darker, so a fold reads as paper.\n        back\n          ? \"bg-[#f1efe9] dark:bg-zinc-800\"\n          : \"bg-[#fbfaf7] dark:bg-zinc-900\",\n        side === \"left\"\n          ? \"left-0 rounded-l-sm shadow-[inset_-1px_0_0_rgb(0_0_0/0.06)]\"\n          : \"left-1/2 rounded-r-sm shadow-[inset_1px_0_0_rgb(0_0_0/0.06)]\",\n        \"ring-1 ring-black/5 dark:ring-white/10\"\n      )}\n      style={style}\n    >\n      {/* The back of a page reads the right way round once it's folded over. */}\n      <div className={cn(\"size-full\", back && \"-scale-x-100\")}>\n        <div className=\"size-full\">{children}</div>\n      </div>\n      {/* The gutter: paper curving into the spine. */}\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-y-0 w-[8%] from-black/[0.07] to-transparent dark:from-black/40\",\n          side === \"left\" ? \"right-0 bg-linear-to-l\" : \"left-0 bg-linear-to-r\"\n        )}\n      />\n    </div>\n  );\n}\n\nfunction NavButton({\n  children,\n  label,\n  disabled,\n  onClick,\n  onKeyDown,\n}: {\n  children: ReactNode;\n  label: string;\n  disabled: boolean;\n  onClick: () => void;\n  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;\n}) {\n  return (\n    <button\n      // aria-disabled, not disabled: the last press keeps focus on the button,\n      // so the arrow keys keep working.\n      aria-disabled={disabled || undefined}\n      aria-label={label}\n      className=\"flex size-8 items-center justify-center rounded-full text-zinc-600 ring-1 ring-black/10 transition-colors duration-150 ease-out hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-emerald-700/50 aria-disabled:cursor-default aria-disabled:text-zinc-300 aria-disabled:hover:bg-transparent aria-disabled:hover:text-zinc-300 dark:text-zinc-300 dark:ring-white/15 dark:aria-disabled:text-zinc-600 dark:hover:bg-white/10 dark:hover:text-white dark:aria-disabled:hover:bg-transparent dark:aria-disabled:hover:text-zinc-600\"\n      onClick={disabled ? undefined : onClick}\n      onKeyDown={onKeyDown}\n      type=\"button\"\n    >\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 16 16\"\n      >\n        {children}\n      </svg>\n    </button>\n  );\n}\n\n/* Demo: a restaurant's spring menu. Sizes are in cqw so a page scales whole. */\n\ninterface Dish {\n  name: string;\n  note: string;\n  price: string;\n}\n\nfunction Course({\n  title,\n  dishes,\n  page,\n}: {\n  title: string;\n  dishes: Dish[];\n  page: number;\n}) {\n  return (\n    <div className=\"flex size-full flex-col px-[9cqw] pt-[10cqw] pb-[7cqw]\">\n      <p className=\"text-[3.4cqw] text-zinc-500 uppercase tracking-[0.2em] dark:text-zinc-400\">\n        {title}\n      </p>\n      <div className=\"mt-[3cqw] h-px w-[10cqw] bg-emerald-800/40 dark:bg-emerald-300/40\" />\n      <ul className=\"mt-[6cqw] flex flex-col gap-[5.5cqw]\">\n        {dishes.map((dish) => (\n          <li key={dish.name}>\n            <p className=\"flex items-baseline gap-[2cqw]\">\n              <span className=\"font-medium text-[4.6cqw] leading-tight\">\n                {dish.name}\n              </span>\n              <span className=\"mb-[0.8cqw] flex-1 self-end border-zinc-300 border-b border-dotted dark:border-zinc-700\" />\n              <span className=\"text-[4cqw] tabular-nums\">{dish.price}</span>\n            </p>\n            <p className=\"mt-[1cqw] text-[3.4cqw] text-zinc-500 leading-snug dark:text-zinc-400\">\n              {dish.note}\n            </p>\n          </li>\n        ))}\n      </ul>\n      <p className=\"mt-auto text-center text-[3cqw] text-zinc-400 tabular-nums dark:text-zinc-500\">\n        {page}\n      </p>\n    </div>\n  );\n}\n\nconst DEFAULT_PAGES: ReactNode[] = [\n  <div\n    className=\"flex size-full flex-col items-center justify-center bg-emerald-900 px-[10cqw] text-center text-[#f3efe4]\"\n    key=\"cover\"\n  >\n    <svg\n      aria-hidden=\"true\"\n      className=\"w-[22cqw]\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.2}\n      viewBox=\"0 0 48 48\"\n    >\n      <circle cx=\"24\" cy=\"24\" r=\"22\" />\n      <path d=\"M24 10c6 6 6 22 0 28M24 10c-6 6-6 22 0 28M10 24h28\" />\n    </svg>\n    <p className=\"mt-[8cqw] font-medium text-[8cqw] leading-none tracking-tight\">\n      Maison Céleste\n    </p>\n    <p className=\"mt-[3cqw] text-[3.4cqw] uppercase tracking-[0.3em] opacity-70\">\n      Spring menu\n    </p>\n    <p className=\"absolute bottom-[8cqw] text-[3cqw] opacity-60\">\n      Turn the corner\n    </p>\n  </div>,\n  <div\n    className=\"flex size-full flex-col px-[10cqw] pt-[12cqw] pb-[7cqw]\"\n    key=\"welcome\"\n  >\n    <p className=\"font-medium text-[6.5cqw] leading-tight tracking-tight\">\n      Welcome\n    </p>\n    <p className=\"mt-[5cqw] text-[3.8cqw] text-zinc-600 leading-relaxed dark:text-zinc-300\">\n      Everything on this menu comes from farms within a morning's drive. It\n      changes when the market does, so ask what came in today.\n    </p>\n    <dl className=\"mt-[8cqw] grid grid-cols-[auto_1fr] gap-x-[5cqw] gap-y-[2.5cqw] text-[3.4cqw]\">\n      <dt className=\"text-zinc-500 dark:text-zinc-400\">Lunch</dt>\n      <dd>Tue–Sat, 12:00–14:30</dd>\n      <dt className=\"text-zinc-500 dark:text-zinc-400\">Dinner</dt>\n      <dd>Tue–Sat, 19:00–22:30</dd>\n      <dt className=\"text-zinc-500 dark:text-zinc-400\">Chef</dt>\n      <dd>Camille Arnaud</dd>\n    </dl>\n    <p className=\"mt-auto text-center text-[3cqw] text-zinc-400 dark:text-zinc-500\">\n      1\n    </p>\n  </div>,\n  <Course\n    dishes={[\n      {\n        name: \"White asparagus\",\n        note: \"Brown butter, hazelnut, chervil\",\n        price: \"16\",\n      },\n      {\n        name: \"Spring pea soup\",\n        note: \"Mint, crème fraîche, sourdough\",\n        price: \"12\",\n      },\n      {\n        name: \"Trout rillettes\",\n        note: \"Pickled radish, rye crisps\",\n        price: \"15\",\n      },\n      { name: \"Burrata\", note: \"Blood orange, fennel pollen\", price: \"17\" },\n    ]}\n    key=\"starters\"\n    page={2}\n    title=\"To start\"\n  />,\n  <Course\n    dishes={[\n      {\n        name: \"Roast chicken\",\n        note: \"Morels, vin jaune, new potatoes\",\n        price: \"32\",\n      },\n      { name: \"Hake\", note: \"Clams, leeks, parsley sauce\", price: \"34\" },\n      {\n        name: \"Lamb shoulder\",\n        note: \"Slow cooked, flageolets, salsa verde\",\n        price: \"36\",\n      },\n      {\n        name: \"Wild garlic gnocchi\",\n        note: \"Pecorino, lemon, broad beans\",\n        price: \"26\",\n      },\n    ]}\n    key=\"mains\"\n    page={3}\n    title=\"Mains\"\n  />,\n  <Course\n    dishes={[\n      { name: \"Grilled leeks\", note: \"Romesco, toasted almonds\", price: \"9\" },\n      { name: \"Glazed carrots\", note: \"Honey, cumin, yogurt\", price: \"8\" },\n      { name: \"Green salad\", note: \"Mustard dressing, herbs\", price: \"7\" },\n      { name: \"Pommes purée\", note: \"A lot of butter\", price: \"7\" },\n    ]}\n    key=\"sides\"\n    page={4}\n    title=\"On the side\"\n  />,\n  <Course\n    dishes={[\n      {\n        name: \"Rhubarb tart\",\n        note: \"Almond cream, vanilla ice cream\",\n        price: \"11\",\n      },\n      { name: \"Chocolate mousse\", note: \"Olive oil, sea salt\", price: \"10\" },\n      {\n        name: \"Strawberries\",\n        note: \"Elderflower, black pepper meringue\",\n        price: \"11\",\n      },\n      { name: \"Cheese\", note: \"Three from the trolley, quince\", price: \"14\" },\n    ]}\n    key=\"desserts\"\n    page={5}\n    title=\"Desserts\"\n  />,\n  <Course\n    dishes={[\n      { name: \"Sancerre\", note: \"Loire, Sauvignon Blanc, 2024\", price: \"13\" },\n      { name: \"Chablis\", note: \"Burgundy, Chardonnay, 2023\", price: \"15\" },\n      { name: \"Morgon\", note: \"Beaujolais, Gamay, 2022\", price: \"12\" },\n      { name: \"Bandol rosé\", note: \"Provence, Mourvèdre, 2024\", price: \"11\" },\n    ]}\n    key=\"wine\"\n    page={6}\n    title=\"By the glass\"\n  />,\n  <div\n    className=\"flex size-full flex-col items-center justify-center bg-emerald-900 px-[10cqw] text-center text-[#f3efe4]\"\n    key=\"back\"\n  >\n    <p className=\"font-medium text-[5cqw] tracking-tight\">Merci</p>\n    <p className=\"mt-[4cqw] text-[3.4cqw] leading-relaxed opacity-70\">\n      14 rue des Vignes, Lyon\n      <br />\n      Book at maisonceleste.fr\n    </p>\n  </div>,\n];\n",
      "path": "/components/kokonutui/page-flip.tsx",
      "target": "components/kokonutui/page-flip.tsx"
    }
  ]
}