# Product Overview 04 - For Skincare Product

> A structured skincare layout featuring image grid previews, descriptive content, pricing details, size selection, and collapsible sections for ingredients, shipping, and product overview information.

A shadcn/ui block from the [@shadcn-space](https://shadcnspace.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-space/product-overview-04).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-space/product-overview-04
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-space/product-overview-04-light-ce4854968b2feccd.png
- Registry item: https://shadcnspace.com/r/product-overview-04.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnspace.com/r/product-overview-04.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `badge`, `button`, `separator`, `accordion`

## Files

### `src/components/ui/toast.tsx`

```tsx
"use client"

import React, {
  useCallback,
  useState,
  useSyncExternalStore,
  useEffect,
  useLayoutEffect,
  isValidElement,
  useRef,
} from "react"
import { createPortal } from "react-dom"
import { createRoot } from "react-dom/client"
import { AnimatePresence, motion } from "motion/react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  XIcon,
  CircleCheckIcon,
  InfoIcon,
  TriangleAlertIcon,
  OctagonXIcon,
  Loader2Icon,
} from "lucide-react"

type ToastType =
  | "default"
  | "success"
  | "error"
  | "info"
  | "warning"
  | "loading"

type ToastPosition =
  | "top-left"
  | "top-center"
  | "top-right"
  | "bottom-left"
  | "bottom-center"
  | "bottom-right"

type ToastMessage = React.ReactNode

type ToastCustomRender = (id: string) => ToastMessage

interface ToastAction {
  label: React.ReactNode
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
  dismiss?: boolean
}

interface ToastOptions {
  id?: string
  type?: ToastType
  title?: ToastMessage
  description?: ToastMessage
  custom?: ToastMessage | ToastCustomRender
  duration?: number
  position?: ToastPosition
  action?: ToastAction
  cancel?: ToastAction
  icon?: React.ReactNode
  closeButton?: boolean
  dismissible?: boolean
  onDismiss?: (id: string) => void
  className?: string
}

interface ToastData extends Omit<ToastOptions, "id" | "type" | "custom"> {
  id: string
  type: ToastType
  custom?: ToastMessage
  createdAt: number
}

interface ToasterProps {
  position?: ToastPosition
  duration?: number
  gap?: number
  offset?: number | Partial<Record<"top" | "right" | "bottom" | "left", number>>
  closeButton?: boolean
  dismissible?: boolean
  icons?: Partial<Record<Exclude<ToastType, "default">, React.ReactNode>>
  toastOptions?: Omit<ToastOptions, "id" | "title" | "type">
  className?: string
  autoMount?: boolean
}

interface PromiseToastData<Value> {
  loading?: ToastMessage | Omit<ToastOptions, "id" | "type">
  success?:
  | ToastMessage
  | Omit<ToastOptions, "id" | "type">
  | ((result: Value) => ToastMessage | Omit<ToastOptions, "id" | "type">)
  error?:
  | ToastMessage
  | Omit<ToastOptions, "id" | "type">
  | ((error: unknown) => ToastMessage | Omit<ToastOptions, "id" | "type">)
}

type ToastCustom = ToastMessage | ((id: string) => ToastMessage)

const defaultIcons: Record<
  Exclude<ToastType, "default">,
  React.ReactNode
> = {
  success: <CircleCheckIcon aria-hidden="true" />,
  info: <InfoIcon aria-hidden="true" />,
  warning: <TriangleAlertIcon aria-hidden="true" />,
  error: <OctagonXIcon className="text-destructive" aria-hidden="true" />,
  loading: <Loader2Icon className="animate-spin" aria-hidden="true" />,
}

const MAX_VISIBLE_TOASTS = 3
const COLLAPSED_PEEK = 14
const SCALE_STEP = 0.06
const TOAST_WIDTH = 380

let toastCounter = 0
let defaultOptions: Partial<ToastOptions> = {}
let inheritPosition: ToastOptions["position"] | undefined
let toastState: ToastData[] = []
let registeredToasters = 0
let autoRoot: ReturnType<typeof createRoot> | null = null
let autoContainer: HTMLDivElement | null = null
let autoMounted = false
let autoConfig: ToasterProps = {}
const listeners = new Set<() => void>()

function emit() {
  for (const listener of listeners) listener()
}

function subscribe(listener: () => void) {
  listeners.add(listener)
  return () => {
    listeners.delete(listener)
  }
}

function getSnapshot() {
  return toastState
}

function ensureMount() {
  if (typeof document === "undefined") return
  if (registeredToasters > 0 || autoMounted) return

  autoMounted = true
  const container = document.createElement("div")
  autoContainer = container
  document.body.appendChild(container)
  autoRoot = createRoot(container)
  autoRoot.render(<Toaster autoMount {...autoConfig} />)
}

function configure(options: ToasterProps) {
  autoConfig = options
  defaultOptions = {
    position: options.position,
    duration: options.duration,
    closeButton: options.closeButton,
    dismissible: options.dismissible,
    ...options.toastOptions,
  }
  if (autoRoot && autoMounted) {
    autoRoot.render(<Toaster autoMount {...autoConfig} />)
  }
}

function generateId() {
  toastCounter += 1
  return `toast-${toastCounter}`
}

function isToastMessage(
  value: ToastMessage | ToastOptions
): boolean {
  return (
    typeof value === "string" ||
    typeof value === "number" ||
    isValidElement(value as React.ReactNode)
  )
}

function removeToast(id: string) {
  const item = toastState.find((toastItem) => toastItem.id === id)
  item?.onDismiss?.(id)
  toastState = toastState.filter((toastItem) => toastItem.id !== id)
  emit()
}

function resolveData(
  value: ToastMessage | Omit<ToastOptions, "id" | "type">
): ToastOptions {
  return isToastMessage(value as ToastMessage)
    ? { title: value as ToastMessage }
    : (value as ToastOptions)
}

function add(
  input: ToastMessage | ToastOptions,
  options?: ToastOptions
): string {
  ensureMount()
  const isMessage = isToastMessage(input)
  const resolved = isMessage
    ? { title: input as ToastMessage }
    : (input as ToastOptions)
  const merged: ToastOptions = {
    ...defaultOptions,
    ...resolved,
    ...(isMessage ? options : undefined),
  }
  const explicitPosition = isMessage ? options?.position : resolved.position
  const position = explicitPosition ?? inheritPosition
  if (position !== undefined) merged.position = position
  inheritPosition = undefined
  const id = merged.id ?? generateId()
  const existing = toastState.find((toastItem) => toastItem.id === id)

  const next: ToastData = {
    id,
    type: merged.type ?? "default",
    title: merged.title,
    description: merged.description,
    custom:
      typeof merged.custom === "function"
        ? (merged.custom as (id: string) => ToastMessage)(id)
        : merged.custom,
    duration: merged.duration,
    position: merged.position,
    action: merged.action,
    cancel: merged.cancel,
    icon: merged.icon,
    closeButton: merged.closeButton,
    dismissible: merged.dismissible,
    onDismiss: merged.onDismiss,
    className: merged.className,
    createdAt: existing ? existing.createdAt : Date.now(),
  }

  toastState = existing
    ? toastState.map((toastItem) =>
      toastItem.id === id
        ? { ...toastItem, ...next, createdAt: toastItem.createdAt }
        : toastItem
    )
    : [...toastState, next]
  emit()
  return id
}

function update(id: string, options: Omit<ToastOptions, "id">) {
  const existing = toastState.find((toastItem) => toastItem.id === id)
  if (!existing) return
  toastState = toastState.map((toastItem) =>
    toastItem.id === id
      ? {
        ...toastItem,
        ...options,
        id,
        type: options.type ?? toastItem.type,
        custom:
          typeof options.custom === "function"
            ? (options.custom as ToastCustomRender)(id)
            : (options.custom ?? toastItem.custom),
      }
      : toastItem
  )
  emit()
}

function dismiss(id?: string) {
  if (!id) {
    toastState = []
    emit()
    return
  }
  removeToast(id)
}

function promise<Value>(
  promise: Promise<Value>,
  data: PromiseToastData<Value>,
  options?: Omit<ToastOptions, "id" | "title" | "type">
): Promise<Value> {
  const loading = resolveData(data.loading ?? "Loading...")
  const id = add({
    ...options,
    ...loading,
    type: "loading",
    duration: loading.duration ?? options?.duration ?? Infinity,
  })

  promise
    .then((result) => {
      const resolved = resolveData(
        typeof data.success === "function"
          ? data.success(result)
          : (data.success ?? "Done")
      )
      update(id, {
        ...options,
        ...resolved,
        type: "success",
        duration:
          resolved.duration ?? options?.duration ?? defaultOptions.duration ?? 5000,
      })
    })
    .catch((error) => {
      const resolved = resolveData(
        typeof data.error === "function"
          ? data.error(error)
          : (data.error ?? "Something went wrong")
      )
      update(id, {
        ...options,
        ...resolved,
        type: "error",
        duration:
          resolved.duration ?? options?.duration ?? defaultOptions.duration ?? 5000,
      })
    })

  return promise
}

function success(
  message: ToastMessage,
  options?: Omit<ToastOptions, "type">
) {
  return add(message, { ...options, type: "success" })
}

function error(message: ToastMessage, options?: Omit<ToastOptions, "type">) {
  return add(message, { ...options, type: "error" })
}

function info(message: ToastMessage, options?: Omit<ToastOptions, "type">) {
  return add(message, { ...options, type: "info" })
}

function warning(message: ToastMessage, options?: Omit<ToastOptions, "type">) {
  return add(message, { ...options, type: "warning" })
}

function loading(message: ToastMessage, options?: Omit<ToastOptions, "type">) {
  return add(message, {
    ...options,
    type: "loading",
    duration: options?.duration ?? Infinity,
  })
}

function custom(
  content: ToastCustom,
  options?: Omit<ToastOptions, "title" | "type">
) {
  return add({ custom: content, ...options })
}

const toast = {
  add,
  update,
  dismiss,
  close: dismiss,
  promise,
  success,
  error,
  info,
  warning,
  loading,
  custom,
  configure,
}

function getOffsetStyle(
  position: ToastPosition,
  offset: NonNullable<ToasterProps["offset"]>
): React.CSSProperties {
  const resolved =
    typeof offset === "number"
      ? { top: offset, right: offset, bottom: offset, left: offset }
      : { top: 16, right: 16, bottom: 16, left: 16, ...offset }

  const style: React.CSSProperties = {}
  if (position.startsWith("top")) style.top = resolved.top
  else style.bottom = resolved.bottom

  if (position.endsWith("left")) style.left = resolved.left
  else if (position.endsWith("right")) style.right = resolved.right
  else {
    style.left = "50%"
    style.transform = "translateX(-50%)"
  }
  return style
}

function groupToasts(
  toasts: ToastData[],
  defaultPosition: ToastPosition
): Map<ToastPosition, ToastData[]> {
  const groups = new Map<ToastPosition, ToastData[]>()
  for (const item of toasts) {
    const pos = item.position ?? defaultPosition
    const group = groups.get(pos)
    if (group) group.push(item)
    else groups.set(pos, [item])
  }
  return groups
}

function ToastCard({
  item,
  position,
  icons,
  y,
  scale,
  zIndex,
  hidden,
  onHeightChange,
  onUnmount,
}: {
  item: ToastData
  position: ToastPosition
  icons?: ToasterProps["icons"]
  y: number
  scale: number
  zIndex: number
  hidden: boolean
  onHeightChange: (id: string, height: number) => void
  onUnmount: (id: string) => void
}) {
  const duration = item.duration ?? defaultOptions.duration ?? 5000
  const autoDismiss = duration > 0 && duration !== Infinity
  const closeButton = item.closeButton ?? defaultOptions.closeButton ?? false
  const dismissible = item.dismissible ?? defaultOptions.dismissible ?? true

  const pausedRef = useRef(false)
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const cardRef = useRef<HTMLDivElement | null>(null)

  const clearTimer = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current)
      timerRef.current = null
    }
  }, [])

  const armTimer = useCallback(() => {
    clearTimer()
    if (!autoDismiss || pausedRef.current) return
    timerRef.current = setTimeout(() => removeToast(item.id), duration)
  }, [autoDismiss, clearTimer, duration, item.id])

  useEffect(() => {
    armTimer()
    return clearTimer
  }, [armTimer, clearTimer])

  const pause = useCallback(() => {
    pausedRef.current = true
    clearTimer()
  }, [clearTimer])

  const resume = useCallback(() => {
    pausedRef.current = false
    armTimer()
  }, [armTimer])

  useLayoutEffect(() => {
    const node = cardRef.current
    if (!node) return
    const report = () => onHeightChange(item.id, node.offsetHeight)
    report()
    if (typeof ResizeObserver === "undefined") return
    const observer = new ResizeObserver(report)
    observer.observe(node)
    return () => observer.disconnect()
  }, [item.id, onHeightChange])

  useEffect(() => {
    return () => onUnmount(item.id)
  }, [item.id, onUnmount])

  const isTop = position.startsWith("top")
  const isLeft = position.endsWith("left")
  const isCenter = position.endsWith("center")
  const restY = isTop ? y : -y

  // Left/right toasts slide in from their edge of the screen; center toasts
  // slide in from the top/bottom edge instead, since they have no side.
  const offScreenY = isTop ? "-100%" : "100%"
  const enterY = isCenter ? `calc(${offScreenY} + ${restY}px)` : restY
  const exitY = enterY
  const restYValue = isCenter ? `calc(0% + ${restY}px)` : restY
  const enterX = isCenter ? 0 : isLeft ? "-100%" : "100%"
  const exitX = enterX

  const icon =
    item.icon ??
    (item.type !== "default"
      ? (icons?.[item.type] ?? defaultIcons[item.type])
      : null)

  return (
    <motion.div
      ref={cardRef}
      initial={{ opacity: 0, x: enterX, y: enterY, scale: scale * 0.95 }}
      animate={{ opacity: hidden ? 0 : 1, x: 0, y: restYValue, scale, zIndex }}
      exit={{ opacity: 0, x: exitX, y: exitY, scale: scale * 0.9 }}
      transition={{ type: "spring", stiffness: 320, damping: 30 }}
      drag={dismissible ? "x" : false}
      dragConstraints={{ left: 0, right: 0 }}
      dragElastic={{ left: 0.4, right: 0.4 }}
      dragSnapToOrigin
      onDragEnd={(_event, info) => {
        if (dismissible && Math.abs(info.offset.x) > 120) {
          removeToast(item.id)
        }
      }}
      onMouseEnter={pause}
      onMouseLeave={resume}
      onFocus={pause}
      onBlur={resume}
      style={{
        position: "absolute",
        left: 0,
        width: "100%",
        [isTop ? "top" : "bottom"]: 0,
        pointerEvents: hidden ? "none" : "auto",
      }}
      className={cn(
        "cursor-default select-none outline-none",
        item.custom
          ? "overflow-visible"
          : "flex items-start gap-3 overflow-hidden rounded-2xl border bg-popover p-4 text-popover-foreground shadow-lg",
        item.className
      )}
    >
      {item.custom ? (
        item.custom
      ) : (
        <>
          {icon ? (
            <span className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4">
              {icon}
            </span>
          ) : null}
          <div className="flex min-w-0 flex-1 flex-col gap-1">
            {item.title ? (
              <div className="text-sm font-medium">{item.title}</div>
            ) : null}
            {item.description ? (
              <div className="text-sm text-muted-foreground">
                {item.description}
              </div>
            ) : null}
          </div>
          {item.action || item.cancel ? (
            <div className="flex shrink-0 items-center gap-2">
              {item.cancel ? (
                <Button
                  variant="ghost"
                  size="sm"
                  className="shrink-0"
                  onClick={(event) => {
                    inheritPosition = item.position
                    item.cancel?.onClick(event)
                    inheritPosition = undefined
                    removeToast(item.id)
                  }}
                >
                  {item.cancel.label}
                </Button>
              ) : null}
              {item.action ? (
                <Button
                  variant="outline"
                  size="sm"
                  className="shrink-0"
                  onClick={(event) => {
                    inheritPosition = item.position
                    item.action?.onClick(event)
                    inheritPosition = undefined
                    if (item.action?.dismiss) removeToast(item.id)
                  }}
                >
                  {item.action.label}
                </Button>
              ) : null}
            </div>
          ) : null}
          {closeButton ? (
            <button
              type="button"
              aria-label="Close toast"
              onClick={() => removeToast(item.id)}
              className="shrink-0 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus:opacity-100 focus:outline-none"
            >
              <XIcon aria-hidden="true" className="size-4" />
            </button>
          ) : null}
        </>
      )}
    </motion.div>
  )
}

function ToastGroup({
  position,
  items,
  icons,
  offset,
  gap,
}: {
  position: ToastPosition
  items: ToastData[]
  icons?: ToasterProps["icons"]
  offset: NonNullable<ToasterProps["offset"]>
  gap: number
}) {
  const [expanded, setExpanded] = useState(false)
  const [heights, setHeights] = useState<Record<string, number>>({})

  const setHeight = useCallback((id: string, height: number) => {
    setHeights((prev) => (prev[id] === height ? prev : { ...prev, [id]: height }))
  }, [])

  const removeHeight = useCallback((id: string) => {
    setHeights((prev) => {
      if (!(id in prev)) return prev
      const next = { ...prev }
      delete next[id]
      return next
    })
  }, [])

  // Newest toast first: it sits at the front of the stack.
  const ordered = [...items].reverse()

  let cumulative = 0
  const offsets = ordered.map((item, index) => {
    const y = expanded
      ? cumulative
      : Math.min(index, MAX_VISIBLE_TOASTS - 1) * COLLAPSED_PEEK
    cumulative += (heights[item.id] ?? 0) + gap
    return y
  })
  const frontHeight = heights[ordered[0]?.id ?? ""] ?? 0
  const expandedHeight = cumulative > 0 ? cumulative - gap : 0

  return (
    <div
      onMouseEnter={() => setExpanded(true)}
      onMouseLeave={() => setExpanded(false)}
      onFocus={() => setExpanded(true)}
      onBlur={() => setExpanded(false)}
      aria-live="polite"
      className="pointer-events-none fixed"
      style={{
        ...getOffsetStyle(position, offset),
        width: `min(${TOAST_WIDTH}px, calc(100vw - 2rem))`,
        height: expanded ? expandedHeight : frontHeight,
        transition: "height 300ms cubic-bezier(0.22, 1, 0.36, 1)",
      }}
    >
      <AnimatePresence initial={false}>
        {ordered.map((item, index) => (
          <ToastCard
            key={item.id}
            item={item}
            position={position}
            icons={icons}
            y={offsets[index]}
            scale={
              expanded ? 1 : 1 - Math.min(index, MAX_VISIBLE_TOASTS) * SCALE_STEP
            }
            zIndex={ordered.length - index}
            hidden={!expanded && index >= MAX_VISIBLE_TOASTS}
            onHeightChange={setHeight}
            onUnmount={removeHeight}
          />
        ))}
      </AnimatePresence>
    </div>
  )
}

function Toaster({
  position = "bottom-right",
  duration = 5000,
  gap = 12,
  offset = 16,
  closeButton = false,
  dismissible = true,
  icons,
  toastOptions,
  className,
  autoMount = false,
}: ToasterProps) {
  const [mounted, setMounted] = useState(false)
  const toasts = useSyncExternalStore(
    subscribe,
    getSnapshot,
    () => []
  )

  useEffect(() => {
    setMounted(true)
    registeredToasters += 1
    defaultOptions = { position, duration, closeButton, dismissible, ...toastOptions }

    if (!autoMount && autoRoot && autoMounted) {
      autoRoot.unmount()
      autoRoot = null
      autoMounted = false
      autoContainer?.remove()
      autoContainer = null
    }

    return () => {
      registeredToasters -= 1
    }
  }, [autoMount, position, duration, closeButton, dismissible, toastOptions])

  if (!mounted) return null

  const groups = groupToasts(toasts, position)

  return createPortal(
    <div className={cn("pointer-events-none fixed inset-0 z-100", className)}>
      {[...groups.entries()].map(([pos, items]) => (
        <ToastGroup
          key={pos}
          position={pos}
          items={items}
          icons={icons}
          offset={offset}
          gap={gap}
        />
      ))}
    </div>,
    document.body
  )
}

export {
  Toaster,
  toast,
  configure,
  type ToasterProps,
  type ToastAction,
  type ToastData,
  type ToastMessage,
  type ToastOptions,
  type ToastPosition,
  type ToastType,
}
```

### `src/components/shadcn-space/blocks/product-overview-04/page.tsx`

```tsx
import ProductOverviewBlock from "@/components/shadcn-space/blocks/product-overview-04";

export default function Page() {
    return <ProductOverviewBlock />
}
```

### `src/components/shadcn-space/blocks/product-overview-04/index.tsx`

```tsx
import ProductOverview, {
    ProductOverviewData,
} from "@/components/shadcn-space/blocks/product-overview-04/product-overview";

const productData: ProductOverviewData = {
    brand: "Aurelia Skin",
    title: "Brewier glow serum",
    rating: 4.8,
    reviewCount: 24,
    description:
        "A lightweight, fast-absorbing serum designed to hydrate, brighten, and restore your skin’s natural glow. Powered by skin-loving botanicals, it helps improve texture, even out tone, and leave your skin looking fresh and radiant every day.",
    discount: 60,
    sizes: [
        { name: "100 ml", price: 100.0 },
        { name: "70 ml", price: 70.0 },
        { name: "50 ml", price: 50.0, outOfStock: true },
    ],
    images: {
        main: "https://images.shadcnspace.com/assets/ecommerce/product-overview/product-overview-04-img-1.webp",
        details: [
            "https://images.shadcnspace.com/assets/ecommerce/product-overview/product-overview-04-img-2.webp",
            "https://images.shadcnspace.com/assets/ecommerce/product-overview/product-overview-04-img-3.webp",
        ],
    },
    accordionInfo: [
        {
            title: "Product overview",
            content:
                "This serum is formulated with high-quality ingredients to provide deep hydration and a visible glow. Suitable for all skin types, it absorbs quickly without leaving a greasy residue.",
        },
        {
            title: "Full ingredient list",
            content:
                "Aqua, Glycerin, Niacinamide, Sodium Hyaluronate, Panthenol, Phenoxyethanol, Ethylhexylglycerin, Citric Acid.",
        },
        {
            title: "Shipping & returns",
            content:
                "We offer free standard shipping on all orders over $50. Returns are accepted within 30 days of purchase for a full refund.",
        },
    ],
};

export default function ProductOverviewBlock() {
    return <ProductOverview product={productData} />;
}
```

### `src/components/shadcn-space/blocks/product-overview-04/product-overview.tsx`

```tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Heart, Minus, Plus, Star } from "lucide-react";
import { toast } from "@/components/ui/toast";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";

export interface ProductOverviewData {
    brand: string;
    title: string;
    rating: number;
    reviewCount: number;
    description: string;
    discount?: number;
    sizes: {
        name: string;
        price: number;
        outOfStock?: boolean;
    }[];
    images: {
        main: string;
        details: string[];
    };
    accordionInfo: {
        title: string;
        content: string;
    }[];
}

export interface ProductOverviewProps {
    product: ProductOverviewData;
}

export default function ProductOverview({
    product,
}: ProductOverviewProps) {
    const {
        brand,
        title,
        rating,
        reviewCount,
        description,
        discount,
        sizes,
        images,
        accordionInfo,
    } = product;
    const [quantity, setQuantity] = React.useState(1);
    const [selectedSize, setSelectedSize] = React.useState(sizes.find(s => !s.outOfStock) || sizes[0]);
    const [isLiked, setIsLiked] = React.useState(false);
    const [isInCart, setIsInCart] = React.useState(false);

    const currentOriginalPrice = selectedSize.price;

    const handleCartToggle = () => {
        if (isInCart) {
            toast.success("product removed from cart");
        } else {
            toast.success("product added to cart");
        }
        setIsInCart(!isInCart);
    };

    const discountedPrice = React.useMemo(() => {
        if (discount !== undefined) {
            return currentOriginalPrice * (1 - discount / 100);
        }
        return currentOriginalPrice;
    }, [currentOriginalPrice, discount]);

    return (
        <section className="w-full bg-background">
            <div className="mx-auto max-w-7xl px-4 py-16 lg:px-12 lg:py-24 xl:px-16">
                <div className="grid grid-cols-1 items-start justify-center gap-6 lg:grid-cols-12">
                    {/* Image Gallery */}
                    <div className="lg:col-span-7">
                        {/* Desktop: Original Layout */}
                        <div className="hidden md:flex flex-col gap-6 md:flex-row h-full">
                            {/* Main Image */}
                            <div className="bg-muted aspect-3/4 flex-1 overflow-hidden rounded-xl md:aspect-auto">
                                <img
                                    src={images.main}
                                    alt={title}
                                    className="size-full object-cover"
                                />
                            </div>
                            {/* Side Images */}
                            <div className="flex w-[270px] flex-col gap-6">
                                {images.details.slice(0, 2).map((img, idx) => (
                                    <div
                                        key={idx}
                                        className="bg-muted flex-1 overflow-hidden rounded-lg"
                                    >
                                        <img
                                            src={img}
                                            alt={`${title} detail ${idx + 1}`}
                                            className="size-full object-cover"
                                        />
                                    </div>
                                ))}
                            </div>
                        </div>

                        {/* Mobile: Unified Slider (Below md breakpoint) */}
                        <div className="scrollbar-hide flex gap-4 overflow-x-auto pb-4 md:hidden">
                            {[images.main, ...images.details].map((img, idx) => (
                                <div
                                    key={idx}
                                    className="bg-muted aspect-[3/4] w-[280px] shrink-0 overflow-hidden rounded-xl sm:w-[350px]"
                                >
                                    <img
                                        src={img}
                                        alt={`${title} image ${idx + 1}`}
                                        className="size-full object-cover"
                                    />
                                </div>
                            ))}
                        </div>
                    </div>

                    {/* Product Details */}
                    <div className="flex flex-col gap-8 lg:col-span-5 lg:pl-6">
                        <div className="flex flex-col gap-5">
                            <div className="flex items-start justify-between gap-4">
                                <div className="flex flex-col gap-3">
                                    <span className="text-muted-foreground text-sm font-medium">
                                        {brand}
                                    </span>
                                    <h2 className="text-foreground text-3xl font-semibold leading-tight tracking-tight lg:text-4xl">
                                        {title}
                                    </h2>
                                    <div className="flex items-center gap-4">
                                        <div className="flex items-center gap-1">
                                            {[...Array(5)].map((_, i) => (
                                                <Star
                                                    key={i}
                                                    className={cn(
                                                        "size-4",
                                                        i < Math.floor(rating)
                                                            ? "fill-orange-400 text-orange-400"
                                                            : "text-muted-foreground/30"
                                                    )}
                                                />
                                            ))}
                                            <span className="ml-1 text-base font-medium">
                                                {rating}
                                            </span>
                                        </div>
                                        <span className="text-foreground text-sm font-medium">
                                            {reviewCount} reviews
                                        </span>
                                    </div>
                                </div>
                                <Button
                                    variant="outline"
                                    size="icon"
                                    className="size-10 shrink-0 rounded-full border-border cursor-pointer"
                                    onClick={() => setIsLiked(!isLiked)}
                                >
                                    <Heart
                                        className={cn(
                                            "size-4 transition-colors",
                                            isLiked && "fill-destructive text-destructive"
                                        )}
                                    />
                                </Button>
                            </div>

                            <p className="text-muted-foreground text-base leading-relaxed">
                                {description}
                            </p>

                            <div className="flex items-center gap-4 py-1">
                                <span className="text-3xl font-semibold">
                                    ${discountedPrice.toFixed(2)}
                                </span>
                                {discount !== undefined && (
                                    <>
                                        <span className="text-muted-foreground text-xl font-semibold line-through opacity-40">
                                            ${currentOriginalPrice.toFixed(2)}
                                        </span>
                                        <Badge
                                            variant="secondary"
                                            className="bg-red-500/10 text-red-500 hover:bg-red-500/20 border-none rounded-full px-3 py-0.5 text-sm"
                                        >
                                            {discount}% Off
                                        </Badge>
                                    </>
                                )}
                            </div>

                            {sizes && sizes.length > 0 && (
                                <div className="flex flex-wrap gap-2">
                                    {sizes.map((size) => (
                                        <Button
                                            key={size.name}
                                            variant={selectedSize.name === size.name ? "default" : "outline"}
                                            size="sm"
                                            disabled={size.outOfStock}
                                            onClick={() => setSelectedSize(size)}
                                            className={cn(
                                                "h-8 rounded-lg px-4 text-sm font-medium transition-all shadow-none",
                                                selectedSize.name === size.name ? "cursor-default" : "cursor-pointer",
                                                selectedSize.name !== size.name && "border-border",
                                                size.outOfStock && "opacity-50 cursor-not-allowed!"
                                            )}
                                        >
                                            {size.name}
                                        </Button>
                                    ))}
                                </div>
                            )}
                        </div>

                        <Separator className="bg-border" />

                        <div className="flex flex-col gap-5">
                            <div className="flex gap-4">
                                <div className="border-border bg-background flex h-12 items-center overflow-hidden rounded-full border shadow-xs">
                                    <Button
                                        variant="ghost"
                                        size="icon"
                                        className="hover:bg-muted h-full shrink-0 rounded-none px-4 cursor-pointer"
                                        onClick={() => setQuantity(Math.max(1, quantity - 1))}
                                    >
                                        <Minus className="size-4" />
                                    </Button>
                                    <div className="border-border flex h-full min-w-14 items-center justify-center border-x text-sm font-medium">
                                        {quantity.toString().padStart(2, "0")}
                                    </div>
                                    <Button
                                        variant="ghost"
                                        size="icon"
                                        className="hover:bg-muted h-full shrink-0 rounded-none px-4 cursor-pointer"
                                        onClick={() => setQuantity(quantity + 1)}
                                    >
                                        <Plus className="size-4" />
                                    </Button>
                                </div>
                                <Button
                                    variant="outline"
                                    className="border-border hover:bg-muted/50 flex-1 rounded-full h-12 font-medium cursor-pointer dark:bg-background shadow-xs"
                                    size="lg"
                                    onClick={handleCartToggle}
                                >
                                    {isInCart ? "Remove from cart" : "Add to cart"}
                                </Button>
                            </div>
                            <Button
                                className="w-full rounded-full h-12 font-medium cursor-pointer hover:bg-primary/80"
                                size="lg"
                            >
                                Buy at ${(discountedPrice * quantity).toFixed(2)}
                            </Button>
                        </div>

                        <div className="flex flex-col pt-2">
                            <Accordion className="w-full">
                                {accordionInfo.map((item, index) => (
                                    <React.Fragment key={index}>
                                        <Separator className="bg-border" />
                                        <AccordionItem value={`item-${index}`} className="border-none">
                                            <AccordionTrigger className="group/trigger py-4 text-base font-normal hover:no-underline **:data-[slot=accordion-trigger-icon]:hidden">
                                                <div className="flex w-full items-center justify-between">
                                                    <span>{item.title}</span>
                                                    <Plus className="size-4 shrink-0 transition-transform duration-500 ease-in-out group-aria-expanded/trigger:rotate-45" />
                                                </div>
                                            </AccordionTrigger>
                                            <AccordionContent className="text-muted-foreground pb-6">
                                                {item.content}
                                            </AccordionContent>
                                        </AccordionItem>
                                    </React.Fragment>
                                ))}
                            </Accordion>
                            <Separator className="bg-border" />
                        </div>
                    </div>
                </div>
            </div>
        </section>
    );
}
```
