{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "foldable",
  "title": "Foldable",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Folded card that opens like the iPhone Duo, blurring as the cover turns.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "slider"
  ],
  "files": [
    {
      "path": "src/registry/components/foldable/foldable.tsx",
      "content": "\"use client\"\n\nimport { createContext, use, useId, useMemo, useRef, useState } from \"react\"\nimport type {\n  ComponentProps,\n  CSSProperties,\n  PointerEvent,\n  ReactNode,\n} from \"react\"\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\"\nimport type { MotionValue } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Slider } from \"@/components/ui/slider\"\n\n/** Pointer travel for a full open, in page widths. */\nconst DRAG_TRAVEL = 1.5\n\n/** How far ahead (s) a flick is projected to pick where the cover lands. */\nconst FLICK_PROJECTION = 0.15\n\n/**\n * The spring aims this far past the end and is stopped as it crosses it, so the\n * cover lands instead of creeping the last pixel. Hence progress is clamped\n * wherever it is read.\n */\nconst LANDING = 0.02\n\n/** Distance from the viewer to the page, in page widths. */\nconst PERSPECTIVE = 4.7\n\n/** How far the free edge rises past the page at most, in page heights. */\nconst MAX_RISE = (PERSPECTIVE / (PERSPECTIVE - 1) - 1) / 2\n\n/** Radius the cover grows to standing up, in page widths (Apple's ratio). */\nconst COVER_RADIUS = 0.12\n\n/**\n * How dark the inside starts, and the last stretch of the opening over which it\n * lights up.\n */\nconst INSIDE_SHADE = 0.75\nconst INSIDE_WAKE = 0.64\n\n/** How a face of the cover blurs, after the shader on Apple's fold. */\ntype FaceFocus = {\n  /** 0 to 1, by progress. */\n  strength: (progress: number) => number\n  /** Blur offset at the hinge and climb per page width, at full strength. */\n  start: number\n  climb: number\n}\n\n/**\n * Near either end, focus eases out ahead of the turn, or a faint dark rim\n * lingers as the cover lands.\n */\nconst SETTLE = 0.02\n\nfunction getSettle(progress: number) {\n  return Math.min(1, Math.min(progress, 1 - progress) / SETTLE)\n}\n\n/** Most blurred standing upright, sharp at the hinge. */\nconst FRONT_FOCUS: FaceFocus = {\n  strength: (p) => Math.min(p, 1 - p) * getSettle(p),\n  start: 0,\n  climb: 2.78,\n}\n\n/** Blurred until well past upright, and a little even at the hinge. */\nconst BACK_FOCUS: FaceFocus = {\n  strength: (p) => clamp01(1.2 * (1 - p)) * getSettle(p),\n  start: 0.23,\n  climb: 2.27,\n}\n\n/** Blur level reached at full strength: a third past Apple's top level, 8. */\nconst LEVEL_RUN = 8 / 0.75\n\n/** Blur levels over which the print fades to black, past the last copy. */\nconst FADE_LEVELS = [7.2, 10.4]\n\n/** How strongly the print dims out from the hinge, per unit of strength. */\nconst DIM_GAIN = 1.5\n\n/**\n * The fade strip is its band plus this many bands of black, and the band\n * stretches to at most this many page widths.\n */\nconst FADE_TAIL = 4\nconst FADE_STRETCH = 8\n\n/**\n * One copy of the print per blur level, each doubling the blur in texels set\n * per face as --foldable-texel. A copy runs past the print so the blur samples\n * paper at the sides and the body's black above and below; the paper stops half\n * a blur short there, so the edge softens without a hard line. Apple's top\n * level, 8, is left to the fade to black.\n */\nconst PRINT_LEVELS = [\n  {\n    level: 0,\n    blur: \"\",\n    body: \"\",\n    bleed: \"inset-0\",\n    paper: \"inset-y-0\",\n    print: \"inset-0\",\n    mask: \"\",\n  },\n  {\n    level: 2,\n    blur: \"blur-[calc(var(--foldable-w)*var(--foldable-texel)*4)]\",\n    body: \"bg-black\",\n    bleed: \"-inset-[calc(var(--foldable-w)*var(--foldable-texel)*8)]\",\n    paper: \"inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*10)]\",\n    print:\n      \"inset-x-[calc(var(--foldable-w)*var(--foldable-texel)*8)] -inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*2)]\",\n    mask: \"mask-size-[calc(100%-var(--foldable-w)*var(--foldable-texel)*16)_calc(100%-var(--foldable-w)*var(--foldable-texel)*16)]\",\n  },\n  {\n    level: 4,\n    blur: \"blur-[calc(var(--foldable-w)*var(--foldable-texel)*16)]\",\n    body: \"bg-black\",\n    bleed: \"-inset-[calc(var(--foldable-w)*var(--foldable-texel)*32)]\",\n    paper: \"inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*40)]\",\n    print:\n      \"inset-x-[calc(var(--foldable-w)*var(--foldable-texel)*32)] -inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*8)]\",\n    mask: \"mask-size-[calc(100%-var(--foldable-w)*var(--foldable-texel)*64)_calc(100%-var(--foldable-w)*var(--foldable-texel)*64)]\",\n  },\n  {\n    level: 6,\n    blur: \"blur-[calc(var(--foldable-w)*var(--foldable-texel)*64)]\",\n    body: \"bg-black\",\n    bleed: \"-inset-[calc(var(--foldable-w)*var(--foldable-texel)*128)]\",\n    paper: \"inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*160)]\",\n    print:\n      \"inset-x-[calc(var(--foldable-w)*var(--foldable-texel)*128)] -inset-y-[calc(var(--foldable-w)*var(--foldable-texel)*32)]\",\n    mask: \"mask-size-[calc(100%-var(--foldable-w)*var(--foldable-texel)*256)_calc(100%-var(--foldable-w)*var(--foldable-texel)*256)]\",\n  },\n]\n\n/**\n * Open flat, the spread has no line down its middle. A ring has no side to\n * leave off, so on the hinge side it runs past the hinge, where the clip cuts\n * it: 2px, as at 1px a stage on a fractional x snaps it back into view.\n */\nconst LINE_WIDTH = 1\nconst LINE_RING = \"inset-ring inset-ring-foreground\"\nconst LINE_PAST_HINGE = {\n  insideRight: \"-left-0.5\",\n  insideLeft: \"-right-0.5 origin-[calc(100%-2px)_50%]\",\n}\n\ntype FreeSide = \"right\" | \"left\"\n\n/** The inner screen spans the spread, so its texel is twice the front's. */\nconst COVER_SIDES = {\n  cover: {\n    slot: \"foldable-cover\",\n    freeSide: \"right\",\n    focus: FRONT_FOCUS,\n    isInside: false,\n    className: \"inset-0 origin-left [--foldable-texel:0.001]\",\n  },\n  insideLeft: {\n    slot: \"foldable-inside-left\",\n    freeSide: \"left\",\n    focus: BACK_FOCUS,\n    isInside: true,\n    className:\n      \"inset-y-0 right-full w-full origin-right [--foldable-texel:0.002]\",\n  },\n} satisfies Record<\n  string,\n  {\n    slot: string\n    freeSide: FreeSide\n    focus: FaceFocus\n    isInside: boolean\n    className: string\n  }\n>\n\nconst MASK_WHOLE = \"linear-gradient(black, black)\"\nconst MASK_NONE = \"linear-gradient(transparent, transparent)\"\n\n/**\n * Page widths from the hinge that stand for \"past the print\": finite, so\n * nothing turns NaN.\n */\nconst BEYOND = 1000\n\nfunction clamp01(value: number) {\n  return Math.min(1, Math.max(0, value))\n}\n\nfunction smoothstep(t: number) {\n  const s = clamp01(t)\n  return s * s * (3 - 2 * s)\n}\n\n/** Off the page, anywhere between lying flat at either end. */\nfunction isTurning(progress: number) {\n  return progress > 0 && progress < 1\n}\n\n/** How much closer the lifted free edge looks: 1 flat, growing as it turns. */\nfunction scaleFreeEdge(progress: number) {\n  return PERSPECTIVE / (PERSPECTIVE - Math.sin(Math.PI * progress))\n}\n\n/**\n * Where the free edge lands on screen, in page widths from the hinge; negative\n * once the back faces the viewer.\n */\nfunction projectFreeEdge(progress: number) {\n  const shift = progress / 2\n  const x = shift + Math.cos(Math.PI * progress)\n\n  // The vanishing point sits at the center of the closed page.\n  return 0.5 + (x - 0.5) * scaleFreeEdge(progress) - shift\n}\n\n/**\n * The cover's corner radius in its own plane, the print's radius lying flat and\n * growing as it stands up, and that corner as projected: taller coming closer,\n * narrower turning away.\n */\nfunction getCoverShape(progress: number) {\n  const scale = scaleFreeEdge(progress)\n  const turn = Math.abs(Math.cos(Math.PI * progress))\n  const radius = `(var(--foldable-radius) + max(0px, var(--foldable-w) * ${COVER_RADIUS} - var(--foldable-radius)) * ${1 - turn})`\n\n  return {\n    scale,\n    radius: `calc${radius}`,\n    rx: `calc(${radius} * ${turn * scale})`,\n    ry: `calc(${radius} * ${scale})`,\n  }\n}\n\nfunction getInsideShade(p: number) {\n  return INSIDE_SHADE * clamp01((1 - p) / INSIDE_WAKE)\n}\n\n/**\n * Page widths from the hinge where a face's blur reaches `level`; BEYOND if it\n * never does.\n */\nfunction getLevelReach(face: FaceFocus, p: number, level: number) {\n  if (level === 0) return -face.start / face.climb\n  const strength = face.strength(clamp01(p))\n  if (strength === 0) return BEYOND\n  return Math.min(\n    (level / LEVEL_RUN / strength - face.start) / face.climb,\n    BEYOND\n  )\n}\n\n// Clamped to what a gradient stop accepts.\nfunction toPercent(x: number) {\n  return `${Math.max(-99, Math.min(99, x)) * 100}%`\n}\n\n/**\n * The print cut to the cover's projected shape; its corners sit above and below\n * by the rise.\n */\nfunction coverClip(freeSide: FreeSide, progress: number) {\n  const { scale, rx, ry } = getCoverShape(progress)\n  const reach = `calc(var(--foldable-h) * ${(1 - scale) / 2})`\n  const edge = projectFreeEdge(progress) * (freeSide === \"right\" ? 1 : -1)\n  const cut = `${(1 - clamp01(edge)) * 100}%`\n\n  return freeSide === \"right\"\n    ? `inset(${reach} ${cut} ${reach} 0 round 0 ${rx} ${rx} 0 / 0 ${ry} ${ry} 0)`\n    : `inset(${reach} 0 ${reach} ${cut} round ${rx} 0 0 ${rx} / ${ry} 0 0 ${ry})`\n}\n\nconst SAMPLES = Array.from({ length: 9 }, (_, i) => i / 8)\n\nfunction byFreeSide(build: (direction: string) => string) {\n  return {\n    right: build(\"to right\"),\n    left: build(\"to left\"),\n  } satisfies Record<FreeSide, string>\n}\n\n// Apple's smoothstep dimming, drawn once; only its opacity animates.\nconst DIM_GRADIENT = byFreeSide(\n  (direction) =>\n    `linear-gradient(${direction}, ${SAMPLES.map(\n      (x) => `rgb(0 0 0 / ${smoothstep(x).toFixed(3)}) ${x * 100}%`\n    ).join(\", \")})`\n)\n\n// A band and then solid black, slid and stretched over FADE_LEVELS.\nconst FADE_GRADIENT = byFreeSide(\n  (direction) =>\n    `linear-gradient(${direction}, ${SAMPLES.map(\n      (x) =>\n        `rgb(0 0 0 / ${smoothstep(x).toFixed(3)}) ${(x / FADE_TAIL) * 100}%`\n    ).join(\", \")}, black)`\n)\n\ntype FoldableContextValue = {\n  /** 0 is closed, 1 is open. */\n  progress: MotionValue<number>\n  /** How dark the inside is: the page, and the inside of the cover. */\n  shade: MotionValue<number>\n}\n\nconst FoldableContext = createContext<FoldableContextValue | null>(null)\n\nfunction useFoldable() {\n  const context = use(FoldableContext)\n  if (!context)\n    throw new Error(\"Foldable parts must be used within <Foldable>.\")\n  return context\n}\n\nexport function Foldable({ className, ...props }: ComponentProps<\"div\">) {\n  const progress = useMotionValue(0)\n  const shade = useTransform(progress, getInsideShade)\n  const value = useMemo(() => ({ progress, shade }), [progress, shade])\n\n  return (\n    <FoldableContext value={value}>\n      <div\n        data-slot=\"foldable\"\n        // Whole-pixel height where the browser can round: the flat parts snap\n        // to the pixel grid and the 3D body does not, so on a fraction their\n        // lines part at the hinge.\n        className={cn(\n          \"flex flex-col items-center gap-6 [--foldable-h:calc(var(--foldable-w)*7/5)] [--foldable-radius:--spacing(4)] [--foldable-surface:var(--background)] [--foldable-w:min(20rem,calc(50vw-2rem))] supports-[height:round(1px,1px)]:[--foldable-h:round(var(--foldable-w)*7/5,1px)]\",\n          className\n        )}\n        {...props}\n      />\n    </FoldableContext>\n  )\n}\n\ntype DragState = {\n  pointerId: number\n  startX: number\n  startProgress: number\n  width: number\n  moved: boolean\n}\n\n/**\n * The paper folds in 3D; the content in `children` stays flat, framed by\n * wherever the paper is.\n */\nexport function FoldableStage({\n  className,\n  children,\n}: {\n  className?: string\n  children: ReactNode\n}) {\n  const { progress } = useFoldable()\n  const shouldReduceMotion = useReducedMotion()\n\n  // Half a page right keeps the spread centered where the closed page sat.\n  const x = useTransform(progress, (p) => `${clamp01(p) * 50}%`)\n  const rotateY = useTransform(progress, [0, 1], [0, -180])\n\n  const [isDragging, setIsDragging] = useState(false)\n  const dragRef = useRef<DragState | null>(null)\n\n  function settle(target: 0 | 1, velocity = 0) {\n    if (shouldReduceMotion) {\n      animate(progress, target, { duration: 0.2, ease: \"easeOut\" })\n      return\n    }\n\n    animate(progress, target === 1 ? 1 + LANDING : -LANDING, {\n      type: \"spring\",\n      bounce: 0,\n      visualDuration: 0.5,\n      velocity,\n      onUpdate: (latest) => {\n        if (target === 1 ? latest < 1 : latest > 0) return\n        progress.stop()\n        progress.set(target)\n      },\n    })\n  }\n\n  function handlePointerDown(event: PointerEvent<HTMLDivElement>) {\n    if (event.button !== 0) return\n\n    event.currentTarget.setPointerCapture(event.pointerId)\n    // Catches the cover mid-turn.\n    progress.stop()\n\n    dragRef.current = {\n      pointerId: event.pointerId,\n      startX: event.clientX,\n      startProgress: progress.get(),\n      width: event.currentTarget.offsetWidth,\n      moved: false,\n    }\n    setIsDragging(true)\n  }\n\n  function handlePointerMove(event: PointerEvent<HTMLDivElement>) {\n    const drag = dragRef.current\n    if (!drag || drag.pointerId !== event.pointerId) return\n\n    const deltaX = event.clientX - drag.startX\n    if (Math.abs(deltaX) > 3) drag.moved = true\n\n    progress.set(\n      clamp01(drag.startProgress - deltaX / (drag.width * DRAG_TRAVEL))\n    )\n  }\n\n  function handlePointerUp(event: PointerEvent<HTMLDivElement>) {\n    const drag = dragRef.current\n    if (!drag || drag.pointerId !== event.pointerId) return\n\n    dragRef.current = null\n    setIsDragging(false)\n\n    const current = progress.get()\n\n    // A tap turns to the other side, with no velocity to carry.\n    if (!drag.moved) {\n      settle(current < 0.5 ? 1 : 0)\n      return\n    }\n\n    const velocity = progress.getVelocity()\n    settle(current + velocity * FLICK_PROJECTION > 0.5 ? 1 : 0, velocity)\n  }\n\n  return (\n    // Sized to the closed page so the vanishing point stays at its center. The\n    // margin, rounded like the height, makes room for the cover rising past it\n    // as it turns. Safari ignores the perspective of a stage it first drew flat,\n    // so the stage keeps its own layer from the start.\n    <div\n      data-slot=\"foldable-stage\"\n      className={cn(\n        \"relative my-[calc(var(--foldable-h)*var(--foldable-rise))] h-(--foldable-h) w-(--foldable-w) will-change-transform supports-[height:round(1px,1px)]:my-[round(var(--foldable-h)*var(--foldable-rise),1px)]\",\n        className\n      )}\n      style={\n        {\n          perspective: `calc(var(--foldable-w) * ${PERSPECTIVE})`,\n          \"--foldable-rise\": MAX_RISE,\n        } as CSSProperties\n      }\n    >\n      {/* Pointer-only; FoldableSlider is the accessible control. */}\n      <motion.div\n        aria-hidden\n        data-dragging={isDragging || undefined}\n        className=\"absolute inset-0 cursor-grab touch-none select-none transform-3d data-dragging:cursor-grabbing\"\n        style={{ x }}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n      >\n        <motion.div\n          className=\"absolute inset-0 origin-left transform-3d\"\n          style={{ rotateY }}\n        >\n          <CoverBody />\n        </motion.div>\n      </motion.div>\n\n      <motion.div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0\"\n        style={{ x }}\n      >\n        {children}\n      </motion.div>\n    </div>\n  )\n}\n\n/**\n * The black wedges above and below the flat content while the cover turns, with\n * their edge line: one sheet seen from both sides, shaped by clip-path because\n * a changing radius repaints.\n */\nfunction CoverBody() {\n  const { progress } = useFoldable()\n  const clipPath = useTransform(progress, (p) => {\n    const { radius } = getCoverShape(clamp01(p))\n    return `inset(0 round 0 ${radius} ${radius} 0)`\n  })\n  const bodyClipPath = useTransform(progress, (p) => {\n    const line = `${LINE_WIDTH}px`\n    const radius = `calc(${getCoverShape(clamp01(p)).radius} - ${line})`\n    return `inset(${line} ${line} ${line} 0 round 0 ${radius} ${radius} 0)`\n  })\n  // Hidden lying flat, where its softer edge would thicken the line; not faded\n  // in, or the line blinks.\n  const opacity = useTransform(progress, (p) => (isTurning(p) ? 1 : 0))\n\n  return (\n    <motion.div\n      className=\"absolute inset-0 overflow-hidden bg-foreground\"\n      style={{ clipPath, opacity }}\n    >\n      <motion.div\n        className=\"absolute inset-0 bg-black\"\n        style={{ clipPath: bodyClipPath }}\n      />\n    </motion.div>\n  )\n}\n\n/** Content of the page the cover closes over: the right half of the spread. */\nexport function FoldableInsideRight({\n  className,\n  children,\n}: {\n  className?: string\n  children: ReactNode\n}) {\n  const { progress, shade } = useFoldable()\n  const clipPath = useTransform(\n    progress,\n    (p) => `inset(0 0 0 ${clamp01(projectFreeEdge(clamp01(p))) * 100}%)`\n  )\n\n  return (\n    <motion.div\n      data-slot=\"foldable-inside-right\"\n      className={cn(\"absolute inset-0\", className)}\n      style={{ clipPath }}\n    >\n      {/* Rounded apart from the reveal, so a thin sliver still ends in the\n          page's corner. */}\n      <div className=\"absolute inset-0 bg-(--foldable-surface) [clip-path:inset(0_round_0_var(--foldable-radius)_var(--foldable-radius)_0)]\">\n        {children}\n        {/* A shade, not a brightness filter, which would repaint each frame. */}\n        <motion.div\n          className=\"absolute inset-0 bg-black\"\n          style={{ opacity: shade }}\n        />\n        {/* Over the shade, so the line is the same at any light. */}\n        <div\n          className={cn(\n            \"absolute inset-y-0 right-0 rounded-r-(--foldable-radius)\",\n            LINE_RING,\n            LINE_PAST_HINGE.insideRight\n          )}\n        />\n      </div>\n    </motion.div>\n  )\n}\n\n/** Content on the outside of the cover, shown while closed. */\nexport function FoldableCover(props: {\n  className?: string\n  children: ReactNode\n}) {\n  return <CoverSide side=\"cover\" {...props} />\n}\n\n/** Content on the inside of the cover: the left half of the spread. */\nexport function FoldableInsideLeft(props: {\n  className?: string\n  children: ReactNode\n}) {\n  return <CoverSide side=\"insideLeft\" {...props} />\n}\n\n/**\n * A side of the cover: blurred copies of the print, dim and fade overlays, and\n * the edge line. Per-frame values only touch transform, opacity, clip-path and\n * mask, never inherited custom properties, which would restyle every element\n * below each frame.\n */\nfunction CoverSide({\n  side,\n  className,\n  children,\n}: {\n  side: keyof typeof COVER_SIDES\n  className?: string\n  children: ReactNode\n}) {\n  const { progress, shade } = useFoldable()\n  const { freeSide, focus, isInside } = COVER_SIDES[side]\n  const direction = freeSide === \"right\" ? \"to right\" : \"to left\"\n  const sign = freeSide === \"right\" ? 1 : -1\n  // Lifted, the free edge can project past the content's width; stretch to meet\n  // it.\n  const scaleX = useTransform(progress, (p) =>\n    Math.max(sign * projectFreeEdge(clamp01(p)), 1)\n  )\n  const clipPath = useTransform(progress, (p) =>\n    coverClip(freeSide, clamp01(p))\n  )\n  // While turning, the print stops a line's width short above and below, so the\n  // body's line runs at full width down to the hinge and meets the page's.\n  const boxClipPath = useTransform(\n    progress,\n    (p) => `inset(${isTurning(p) ? LINE_WIDTH : 0}px 0)`\n  )\n  // A clip alone does not reliably keep a turned-away side's blurred copies\n  // from being drawn.\n  const visibility = useTransform(progress, (p) =>\n    sign * projectFreeEdge(clamp01(p)) > 0 ? \"visible\" : \"hidden\"\n  )\n  // The line follows the projected edge: thinning as the face turns away,\n  // taller as it rises.\n  const edgeScaleX = useTransform(progress, (p) =>\n    clamp01(sign * projectFreeEdge(clamp01(p)))\n  )\n  const edgeScaleY = useTransform(progress, (p) => scaleFreeEdge(clamp01(p)))\n  const dimOpacity = useTransform(progress, (p) =>\n    Math.min(1, DIM_GAIN * focus.strength(clamp01(p)))\n  )\n  // One page wide and stretched by scaleX, which takes far less texture than a\n  // strip as long as its tail.\n  const fadeX = useTransform(progress, (p) => {\n    const from = getLevelReach(focus, p, FADE_LEVELS[0])\n    return `${sign * Math.min(from, FADE_STRETCH * FADE_TAIL) * 100}%`\n  })\n  const fadeScaleX = useTransform(progress, (p) => {\n    const [from, to] = FADE_LEVELS.map((level) =>\n      getLevelReach(focus, p, level)\n    )\n    return Math.min(Math.max(to - from, 0.01), FADE_STRETCH) * FADE_TAIL\n  })\n\n  return (\n    <motion.div\n      data-slot={COVER_SIDES[side].slot}\n      className={cn(\"absolute\", COVER_SIDES[side].className, className)}\n      style={{ scaleX, visibility, clipPath: boxClipPath }}\n    >\n      <motion.div\n        className=\"absolute inset-0 bg-(--foldable-surface)\"\n        style={{ clipPath }}\n      >\n        {PRINT_LEVELS.map((step, index) => (\n          <PrintCopy\n            key={step.level}\n            focus={focus}\n            direction={direction}\n            index={index}\n          >\n            {children}\n          </PrintCopy>\n        )).reverse()}\n        {/* The overlays run a pixel past the box like the copies' paper, so one\n            cut ends them on the same row; ending on the box, a row of unlit\n            paper shows at fractional positions. */}\n        <motion.div\n          className=\"absolute inset-x-0 -inset-y-px\"\n          style={{\n            backgroundImage: DIM_GRADIENT[freeSide],\n            opacity: dimOpacity,\n          }}\n        />\n        <motion.div\n          className={cn(\n            \"absolute -inset-y-px w-full will-change-transform\",\n            freeSide === \"right\" ? \"left-0 origin-left\" : \"right-0 origin-right\"\n          )}\n          style={{\n            backgroundImage: FADE_GRADIENT[freeSide],\n            x: fadeX,\n            scaleX: fadeScaleX,\n          }}\n        />\n        {isInside && (\n          <motion.div\n            className=\"absolute inset-x-0 -inset-y-px bg-black\"\n            style={{ opacity: shade }}\n          />\n        )}\n        <motion.div\n          className={cn(\n            \"absolute inset-y-0 left-0\",\n            LINE_RING,\n            freeSide === \"right\"\n              ? \"right-0 origin-left rounded-r-(--foldable-radius)\"\n              : cn(\"rounded-l-(--foldable-radius)\", LINE_PAST_HINGE.insideLeft)\n          )}\n          style={{ scaleX: edgeScaleX, scaleY: edgeScaleY }}\n        />\n      </motion.div>\n    </motion.div>\n  )\n}\n\n/**\n * The print at one blur level, shown from where the blur climbs to it until the\n * next level takes over. It stays hidden before that, or its dark edge would\n * show through the copy above as a fringe.\n */\nfunction PrintCopy({\n  focus,\n  direction,\n  index,\n  children,\n}: {\n  focus: FaceFocus\n  direction: string\n  /** Index into PRINT_LEVELS, from the sharpest. */\n  index: number\n  children: ReactNode\n}) {\n  const { progress } = useFoldable()\n  const step = PRINT_LEVELS[index]\n  const maskImage = useTransform(progress, (p) => {\n    const reach = (i: number) => getLevelReach(focus, p, PRINT_LEVELS[i].level)\n    if (index > 0 && reach(index) === BEYOND) return MASK_NONE\n\n    const stops = [`black ${toPercent(reach(index))}`]\n    if (index >= 2) {\n      stops.unshift(\n        `transparent ${toPercent(reach(index - 2))}`,\n        `black ${toPercent(reach(index - 1))}`\n      )\n    }\n    if (index < PRINT_LEVELS.length - 1 && reach(index + 1) !== BEYOND) {\n      stops.push(`transparent ${toPercent(reach(index + 1))}`)\n    }\n\n    return stops.length > 1\n      ? `linear-gradient(${direction}, ${stops.join(\", \")})`\n      : MASK_WHOLE\n  })\n\n  return (\n    <motion.div\n      className={cn(\n        \"absolute mask-center mask-no-repeat\",\n        step.body,\n        step.bleed,\n        step.blur,\n        step.mask\n      )}\n      style={{ maskImage }}\n    >\n      <div\n        className={cn(\n          \"absolute inset-x-0 overflow-hidden bg-(--foldable-surface)\",\n          step.paper\n        )}\n      >\n        <div className={cn(\"absolute\", step.print)}>{children}</div>\n      </div>\n    </motion.div>\n  )\n}\n\nexport function FoldableSlider({ className }: { className?: string }) {\n  const labelId = useId()\n  const { progress } = useFoldable()\n\n  // Controlled, as the cover also moves by drag and spring. Only this\n  // re-renders as it turns.\n  const [value, setValue] = useState(0)\n  useMotionValueEvent(progress, \"change\", (p) => setValue(clamp01(p)))\n\n  return (\n    <div className={cn(\"flex w-64 flex-col gap-3\", className)}>\n      <p id={labelId} className=\"text-sm text-muted-foreground\">\n        Move slider to open and close.\n      </p>\n      <Slider\n        aria-labelledby={labelId}\n        min={0}\n        max={1}\n        step={0.001}\n        largeStep={0.1}\n        value={[value]}\n        onValueChange={(next) => {\n          progress.stop()\n          progress.set(typeof next === \"number\" ? next : next[0])\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/foldable.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/foldable",
  "categories": [
    "illustrations"
  ],
  "type": "registry:component"
}