loading
Just a moment.

Draggable Curved Menu

November 2024

Dancing on the Moon Chair
Curved Steps on Panton Sea
Table in the Sky of Saarinen
Egg Flight and Feathers
Whispers of Colombo in the Mist
Chairs Dreaming of Djinns
Orange that Spins and Laughs
Nelson's Coconut and Tide
Coffee in Noguchi’s Fog
Coffee on Platner's Horizon
Time Lounge by Newson
Silence in Eame's Lounge
Chameleon on Bellini's Sofa
Aarnio's Flying Pastilles
Cardin's Table in the Cosmos
Aalto's Vase in the Wind
Dreams of the Chaise Longue
Bibendum's Song
Plastic Tales by Eames
Olivetti's Synthesis Chair
Echoes of Life by Piretti
Bonetto's Boomerang Desk
Tizio's Light in the Night
Magistretti's Sofa Dreams
Garden Egg in the Dawn
Paulin's Globe on the Horizon
Colani's Rotor Table
Lovegrove's Go Chair
Arad's Chair in the Breeze
import {
  PanInfo,
  motion,
  useAnimation,
  useMotionValue,
  useMotionValueEvent,
  useTransform,
} from 'framer-motion';
import { useRef, useState } from 'react';

const menuItems = [
  `Dancing on the Moon Chair`,
  `Curved Steps on Panton Sea`,
  `Table in the Sky of Saarinen`,
  `Egg Flight and Feathers`,
  `Whispers of Colombo in the Mist`,
  `Chairs Dreaming of Djinns`,
  `Orange that Spins and Laughs`,
  `Nelson's Coconut and Tide`,
  `Coffee in Noguchi’s Fog`,
  `Coffee on Platner's Horizon`,
  `Time Lounge by Newson`,
  `Silence in Eame's Lounge`,
  `Chameleon on Bellini's Sofa`,
  `Aarnio's Flying Pastilles`,
  `Cardin's Table in the Cosmos`,
  `Aalto's Vase in the Wind`,
  `Dreams of the Chaise Longue`,
  `Bibendum's Song`,
  `Plastic Tales by Eames`,
  `Olivetti's Synthesis Chair`,
  `Echoes of Life by Piretti`,
  `Bonetto's Boomerang Desk`,
  `Tizio's Light in the Night`,
  `Magistretti's Sofa Dreams`,
  `Garden Egg in the Dawn`,
  `Paulin's Globe on the Horizon`,
  `Colani's Rotor Table`,
  `Lovegrove's Go Chair`,
  `Arad's Chair in the Breeze`,
];

const angleIncrement = 360 / menuItems.length;
const dragFactor = 0.01;

const DraggableCurvedMenu = () => {
  const controls = useAnimation();
  const rotation = useMotionValue(0);
  const containerRef = useRef(null);
  const [middleItem, setMiddleItem] = useState(menuItems[0]);

  useMotionValueEvent(rotation, 'change', (value) => {
    const adjustedRotation = ((value % 360) + 360) % 360;
    const middleIndex =
      Math.round(adjustedRotation / angleIncrement) % menuItems.length;
    const actualMiddleItem =
      menuItems[(menuItems.length - middleIndex) % menuItems.length];
    setMiddleItem(actualMiddleItem);
  });

  const onDrag = (_, info) => {
    const currentRotation = rotation.get() + info.offset.y * dragFactor;
    rotation.set(currentRotation);
  };

  const onDragEnd = (_, info) => {
    const endRotation = rotation.get() + info.velocity.y * dragFactor;
    controls.start({
      rotate: endRotation,
      transition: { type: 'spring', mass: 0.1 },
    });
  };

  const transform = useTransform(rotation, (value) => {
    return `rotate(${value}deg)`;
  });

  return (
    <div
      className="relative flex h-[500px] w-full items-center justify-center overflow-hidden"
      ref={containerRef}
    >
      <div className="pointer-events-none absolute left-0 top-0 z-50 h-32 w-full bg-neutral-100 to-transparent backdrop-blur-xl [-webkit-mask-image:linear-gradient(to_bottom,black,transparent)] dark:bg-neutral-900"></div>
      <motion.div
        className="relative -ml-[1200px] flex h-[1000px] w-[1000px] cursor-grab items-center justify-center active:cursor-grabbing"
        animate={controls}
        style={{
          transformOrigin: 'center center',
          transform,
          rotate: rotation,
        }}
        drag="y"
        onDrag={onDrag}
        onDragEnd={onDragEnd}
      >
        {menuItems.map((item, index) => {
          const rotate = angleIncrement * index;
          return (
            <motion.div
              key={`${item}-${index}`}
              className={`absolute ${item === middleItem
                ? 'text-primary-light-12 dark:text-primary-dark-12'
                : 'text-primary-light-12/30 dark:text-primary-dark-12/30'
                } transition-colors duration-150`}
              style={{
                left: '50%',
                transform: `rotate(${rotate}deg) translateX(300px)`,
                transformOrigin: 'left center',
              }}
            >
              {item}
            </motion.div>
          );
        })}
      </motion.div>
      <div className="pointer-events-none absolute bottom-0 left-0 z-50 h-32 w-full bg-neutral-100 to-transparent backdrop-blur-xl [-webkit-mask-image:linear-gradient(to_top,black,transparent)] dark:bg-neutral-900"></div>
    </div>
  );
};

export default DraggableCurvedMenu;