book

PreviousNext
Docs
aliimamcomponent

Preview

Loading preview…
registry/default/components/book.tsx
"use client"

import React from "react"

import { cn } from "@/registry/default/lib/utils"

interface BookProps {
  children: React.ReactNode
  color?: string
  textColor?: string
  texture?: boolean
  depth?: number
  variant?: "default" | "simple" | "hardcover" | "notebook"
  illustration?: React.ReactNode
  width?: number
  height?: number
  spineText?: string
  bookmark?: boolean
  bookmarkColor?: string
  animation?: "hover" | "float" | "pulse" | "flip" | "none"
  size?: "xs" | "sm" | "md" | "lg" | "xl"
  orientation?: "portrait" | "landscape"
  pages?: number
  author?: string
  title?: string
}

const sizePresets = {
  xs: { width: 120, height: 160 },
  sm: { width: 150, height: 200 },
  md: { width: 196, height: 240 },
  lg: { width: 240, height: 320 },
  xl: { width: 300, height: 400 },
}

export function Book({
  children,
  color = "#555555",
  textColor = "currentColor",
  depth = 5,
  variant = "default",
  illustration,
  width,
  height,
  spineText,
  bookmark = false,
  bookmarkColor = "#fff200",
  animation = "hover",
  size = "md",
  orientation = "portrait",
  pages = 100,
  author,
  title,
}: BookProps) {
  const dimensions = sizePresets[size]
  const bookWidth = width || dimensions.width
  const bookHeight = height || dimensions.height

  const getVariantStyles = () => {
    switch (variant) {
      case "hardcover":
        return {
          borderRadius: "4px",
          border: "2px solid rgba(0,0,0,0.1)",
        }
      case "notebook":
        return {
          borderRadius: "6px",
          background: `linear-gradient(135deg, ${color} 0%, ${color}dd 100%)`,
        }
      default:
        return {
          borderRadius: "4px",
        }
    }
  }

  const getAnimationClass = () => {
    switch (animation) {
      case "hover":
        return "group-hover:[transform:rotateY(-25deg)_scale(1.05)_translateX(-12px)]"
      case "none":
        return ""
      default:
        return "group-hover:[transform:rotateY(-20deg)_scale(1.066)_translateX(-8px)]"
    }
  }

  const bindBg =
    variant === "notebook"
      ? "linear-gradient(90deg, transparent 0%, rgba(255,0,0,0.3) 2%, transparent 4%, transparent 96%, rgba(255,0,0,0.5) 98%, transparent 100%)"
      : "linear-gradient(90deg,hsla(0,0%,100%,0),hsla(0,0%,100%,0) 12%,hsla(0,0%,100%,.25) 29.25%,hsla(0,0%,100%,0) 50.5%,hsla(0,0%,100%,0) 75.25%,hsla(0,0%,100%,.25) 91%,hsla(0,0%,100%,0)),linear-gradient(90deg,rgba(0,0,0,.03),rgba(0,0,0,.1) 12%,transparent 30%,rgba(0,0,0,.02) 50%,rgba(0,0,0,.2) 73.5%,rgba(0,0,0,.5) 75.25%,rgba(0,0,0,.15) 85.25%,transparent)"

  const pageGradient =
    variant === "notebook"
      ? "repeating-linear-gradient(90deg, #f8f9fa 0px, #e9ecef 1px, #f8f9fa 2px, #dee2e6 3px)"
      : "repeating-linear-gradient(90deg,#fff,#a3a3a3 1px,#fff 4px,#9a9a9a 0)"

  const variantStyles = getVariantStyles()

  return (
    <div
      className={cn(
        "group inline-block w-fit cursor-pointer transition-all duration-300 [perspective:1200px]"
      )}
      style={
        {
          "--book-color": color,
          "--text-color": textColor,
          "--book-depth": `${depth}cqw`,
          "--book-width": `${bookWidth}px`,
          "--book-height": `${bookHeight}px`,
        } as React.CSSProperties
      }
    >
      <div
        className={cn(
          "relative w-fit min-w-[calc(var(--book-width))] rotate-0 transition-all duration-700 ease-out contain-inline-size [transform-style:preserve-3d]",
          orientation === "landscape" ? "aspect-[4/3]" : "aspect-[3/4]",
          getAnimationClass()
        )}
      >
        {/* Main Book Cover */}
        <div
          className="absolute size-full overflow-hidden border"
          style={{
            backgroundColor: color,
            ...variantStyles,
            height: bookHeight,
          }}
        >
          {/* Bookmark Ribbon */}
          {bookmark && (
            <div
              className="absolute top-0 right-4 z-10 h-16 w-4 shadow-sm"
              style={{
                backgroundColor: bookmarkColor,
                clipPath: "polygon(0 0, 100% 0, 100% 90%, 50% 100%, 0 90%)",
              }}
            />
          )}

          {/* Spine Text */}
          {spineText && (
            <div
              className="absolute top-1/2 left-0 z-10 -translate-y-1/2 -rotate-90 px-2 text-xs font-semibold whitespace-nowrap"
              style={{ color: textColor, transformOrigin: "center" }}
            >
              {spineText}
            </div>
          )}

          {/* Cover Content */}
          {variant !== "simple" && (
            <div
              className="relative flex h-full flex-col overflow-hidden"
              style={{ backgroundColor: color }}
            >
              {/* Bind Effect */}
              <div
                className="absolute inset-y-0 left-0 w-[10%] opacity-100 mix-blend-overlay"
                style={{ backgroundImage: bindBg }}
              />

              {/* Title and Author */}
              {(title || author) && (
                <div className="p-4 text-center">
                  {title && (
                    <h3
                      className={cn("mb-2 text-lg font-bold")}
                      style={{ color: textColor }}
                    >
                      {title}
                    </h3>
                  )}
                  {author && (
                    <p
                      className="absolute bottom-2 left-7 text-xs opacity-70"
                      style={{ color: textColor }}
                    >
                      {author}
                    </p>
                  )}
                </div>
              )}

              {/* Illustration */}
              {illustration && (
                <div className="flex flex-1 items-center justify-center p-4">
                  {illustration}
                </div>
              )}

              {/* Content */}
              <div className="flex-1 p-4">
                <div className="h-full w-full">{children}</div>
              </div>

              {/* Page Count */}
              {pages && (
                <div
                  className="absolute right-2 bottom-2 text-xs opacity-70"
                  style={{ color: textColor }}
                >
                  {pages}p
                </div>
              )}
            </div>
          )}

          {/* Simple Variant Content */}
          {variant === "simple" && (
            <div className="flex h-full items-center justify-center p-4">
              {children}
            </div>
          )}
        </div>

        {/* Book Pages (Side) */}
        <div
          aria-hidden={true}
          className="absolute top-[3px] h-[calc(88%)] w-[calc(var(--book-depth)-2px)]"
          style={{
            backgroundImage: pageGradient,
            transform: `translateX(calc(var(--book-width) - var(--book-depth) / 2 - 6px)) rotateY(90deg) translateX(calc(var(--book-depth) / 2))`,
          }}
        />

        {/* Book Back Cover */}
        <div
          aria-hidden={true}
          className="absolute left-0 h-[calc(90%)] w-full"
          style={{
            backgroundColor: color,
            ...variantStyles,
            transform: "translateZ(calc(-1 * var(--book-depth)))",
            filter: "brightness(0.8)",
          }}
        />
      </div>
    </div>
  )
}

Installation

npx shadcn@latest add @aliimam/book

Usage

import { Book } from "@/components/book"
<Book />