drag-items

PreviousNext
Docs
ui-layoutscomponent

Preview

Loading preview…
./registry/components/drag/drag-items.tsx
'use client';
import React, { useEffect, useState } from 'react';
import {
  useMotionValue,
  Reorder,
  useDragControls,
  motion,
  MotionValue,
  animate,
  DragControls,
} from 'motion/react';

export default function index() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'Follow me on X',
      subtitle: 'Get more update follow me on X',
      date: '32.8.2024',
      src: 'https://images.unsplash.com/photo-1724908549265-06972c22ca37?q=80&w=2070&auto=format&fit=crop',
      link: 'https://x.com/naymur_dev',
    },
    {
      id: 2,
      title: 'Star the repo',
      subtitle: 'If you like star the repo',
      date: '32.8.2024',
      src: 'https://images.unsplash.com/photo-1724908549265-06972c22ca37?q=80&w=2070&auto=format&fit=crop',
      link: 'https://github.com/ui-layouts/uilayouts',
    },
    {
      id: 3,
      title: 'Share it with your friend',
      subtitle: 'if you like it, please Share it with your friend ',
      date: '32.8.2024',
      src: 'https://images.unsplash.com/photo-1724908549265-06972c22ca37?q=80&w=2070&auto=format&fit=crop',
      link: 'https://www.ui-layouts.com/',
    },
  ]);

  return (
    <Reorder.Group
      axis='y'
      values={items}
      onReorder={setItems}
      className='space-y-2 w-full max-w-md mx-auto'
    >
      {items.map((item) => (
        <Item key={item.id} item={item}>
          <h1 className='text-xl'>{item.title}</h1>
          <p className='text-sm'>{item.subtitle}</p>
          <span>{item.date}</span>
          <a
            className='block w-fit p-2 px-3 text-xs rounded-md  bg-primary-foreground text-primary'
            href={item.link}
          >
            Click
          </a>
        </Item>
      ))}
    </Reorder.Group>
  );
}

const Item = ({ children, item }: { children: React.ReactNode; item: any }) => {
  const y = useMotionValue(0);
  const boxShadow = useRaisedShadow(y);
  const dragControls = useDragControls();
  const [isDragging, setIsDragging] = useState(false);
  const [pressed, setPressed] = useState(false);

  return (
    <Reorder.Item
      value={item}
      style={{ boxShadow, y }}
      dragListener={false}
      dragControls={dragControls}
      onDragStart={() => setIsDragging(true)}
      onDragEnd={() => {
        setIsDragging(false);
        setPressed(false);
      }}
      onPointerUp={() => setPressed(false)}
      onPointerCancel={() => setPressed(false)}
      className='flex justify-between items-center w-full p-3 text-primary-foreground bg-primary border rounded-md'
    >
      <div>{children}</div>
      <ReorderIcon
        dragControls={dragControls}
        isActive={isDragging || pressed}
        onPress={() => setPressed(true)}
      />
    </Reorder.Item>
  );
};

interface ReorderIconProps {  
  dragControls: DragControls;
  isActive: boolean;
  onPress: () => void;
}

export function ReorderIcon({ dragControls, isActive, onPress }: ReorderIconProps) {
  return (
    <motion.button
      type="button"
      aria-label="Reorder"
      animate={{ scale: isActive ? 0.85 : 1 }}
      transition={{ type: 'spring', stiffness: 500, damping: 30 }}
      onPointerDown={(e) => {
        e.preventDefault();
        onPress();
        dragControls.start(e);
      }}
      className="cursor-grab active:cursor-grabbing"
      style={{ touchAction: 'none' }}
    >
      <svg
        xmlns='http://www.w3.org/2000/svg'
        viewBox='0 0 39 39'
        width='39'
        height='39'
        className='fill-primary-foreground'
      >
        <path d='M 5 0 C 7.761 0 10 2.239 10 5 C 10 7.761 7.761 10 5 10 C 2.239 10 0 7.761 0 5 C 0 2.239 2.239 0 5 0 Z'></path>
        <path d='M 19 0 C 21.761 0 24 2.239 24 5 C 24 7.761 21.761 10 19 10 C 16.239 10 14 7.761 14 5 C 14 2.239 16.239 0 19 0 Z'></path>
        <path d='M 33 0 C 35.761 0 38 2.239 38 5 C 38 7.761 35.761 10 33 10 C 30.239 10 28 7.761 28 5 C 28 2.239 30.239 0 33 0 Z'></path>
        <path d='M 33 14 C 35.761 14 38 16.239 38 19 C 38 21.761 35.761 24 33 24 C 30.239 24 28 21.761 28 19 C 28 16.239 30.239 14 33 14 Z'></path>
        <path d='M 19 14 C 21.761 14 24 16.239 24 19 C 24 21.761 21.761 24 19 24 C 16.239 24 14 21.761 14 19 C 14 16.239 16.239 14 19 14 Z'></path>
        <path d='M 5 14 C 7.761 14 10 16.239 10 19 C 10 21.761 7.761 24 5 24 C 2.239 24 0 21.761 0 19 C 0 16.239 2.239 14 5 14 Z'></path>
        <path d='M 5 28 C 7.761 28 10 30.239 10 33 C 10 35.761 7.761 38 5 38 C 2.239 38 0 35.761 0 33 C 0 30.239 2.239 28 5 28 Z'></path>
        <path d='M 19 28 C 21.761 28 24 30.239 24 33 C 24 35.761 21.761 38 19 38 C 16.239 38 14 35.761 14 33 C 14 30.239 16.239 28 19 28 Z'></path>
        <path d='M 33 28 C 35.761 28 38 30.239 38 33 C 38 35.761 35.761 38 33 38 C 30.239 38 28 35.761 28 33 C 28 30.239 30.239 28 33 28 Z'></path>
      </svg>
    </motion.button>
  );
}

const inactiveShadow = '0px 0px 0px rgba(0,0,0,0.8)';

export function useRaisedShadow(value: MotionValue<number>) {
  const boxShadow = useMotionValue(inactiveShadow);

  useEffect(() => {
    let isActive = false;
    value.onChange((latest) => {
      const wasActive = isActive;
      if (latest !== 0) {
        isActive = true;
        if (isActive !== wasActive) {
          animate(boxShadow, '5px 5px 10px rgba(0,0,0,0.3)');
        }
      } else {
        isActive = false;
        if (isActive !== wasActive) {
          animate(boxShadow, inactiveShadow);
        }
      }
    });
  }, [value, boxShadow]);

  return boxShadow;
}

Installation

npx shadcn@latest add @ui-layouts/drag-items

Usage

import { DragItems } from "@/components/drag-items"
<DragItems />