# Hotel Room Cards Sliding To A Second View

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/hotel-room-cards-sliding-to-a-second-view).

- Page, with a live preview: https://shadcn.exchange/r/weinc/hotel-room-cards-sliding-to-a-second-view
- Screenshot: https://shadcn.exchange/shot/weinc/hotel-room-cards-sliding-to-a-second-view/light.png
- Registry item: https://we.inc/r/hotel-room-cards-sliding-to-a-second-view.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/hotel-room-cards-sliding-to-a-second-view.json
```

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

## Files

### `components/we-inc/hotel-room-cards-sliding-to-a-second-view.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

const DELAY = 2200; // ms between idle swaps while nobody is pointing or tabbing

const rooms = [
  {
    name: "Shore Room",
    note: "Queen bed, sheer curtains, a step out to the terrace",
    rate: "From $210 a night",
    a: { src: "https://images.pexels.com/photos/2899687/pexels-photo-2899687.jpeg?auto=compress&cs=tinysrgb&w=800", alt: "A hotel room with sheer curtains open onto a terrace and palm trees", label: "The room" },
    b: { src: "https://images.pexels.com/photos/3225561/pexels-photo-3225561.jpeg?auto=compress&cs=tinysrgb&w=800", alt: "Glass doors opening onto a balcony with chairs and palm trees outside", label: "The balcony" },
  },
  {
    name: "Ridge Room",
    note: "Low bed, reading chair, a west window for the evening light",
    rate: "From $180 a night",
    a: { src: "https://images.pexels.com/photos/2346091/pexels-photo-2346091.jpeg?auto=compress&cs=tinysrgb&w=800", alt: "A dim bedroom whose window looks out on mountains at sunset", label: "The room" },
    b: { src: "https://images.pexels.com/photos/18311189/pexels-photo-18311189.jpeg?auto=compress&cs=tinysrgb&w=800", alt: "A green alpine valley below snowy mountain peaks", label: "The valley" },
  },
];

const css = `
.w8rom-frame { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 14px; background: #cbd5e1; }
.w8rom-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .8s cubic-bezier(.65,0,.2,1); }
.w8rom-b { transform: translateX(100%); }
.w8rom-chip { position: absolute; left: 14px; bottom: 14px; height: 30px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,.92); color: #0f172a; font-size: 13px; font-weight: 600; }
.w8rom-chip span { display: block; height: 30px; line-height: 30px; padding: 0 12px; transition: transform .6s cubic-bezier(.65,0,.2,1); }
.w8rom-card[data-on] .w8rom-a { transform: translateX(-35%); }
.w8rom-card[data-on] .w8rom-b { transform: none; }
.w8rom-card[data-on] .w8rom-chip span { transform: translateY(-100%); }
@media (hover: hover) and (pointer: fine) {
  .w8rom-card:hover .w8rom-a { transform: translateX(-35%); }
  .w8rom-card:hover .w8rom-b { transform: none; }
  .w8rom-card:hover .w8rom-chip span { transform: translateY(-100%); }
}
.w8rom-btn:focus-visible, .w8rom-link:focus-visible { outline: 2px solid #0f172a; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .w8rom-img, .w8rom-chip span { transition: none; } }
`;

export default function HotelRoomCardsSlidingToASecondView() {
  const root = useRef<HTMLElement>(null);
  const [on, setOn] = useState<boolean[]>([true, false]);
  const live = useRef(true);

  useEffect(() => {
    const el = root.current;
    if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let timer = 0, visible = true, step = 0;
    const run = () => {
      window.clearInterval(timer);
      timer = 0;
      if (live.current && visible) timer = window.setInterval(() => { step = (step + 1) % 4; setOn([step === 0, step === 2]); }, DELAY);
    };
    // a real mouse, a tap on a button, or the keyboard takes over from the idle demo
    const stop = () => { if (!live.current) return; live.current = false; setOn([false, false]); run(); };
    const enter = (e: PointerEvent) => { if (e.pointerType === "mouse") stop(); };
    const io = new IntersectionObserver(([en]) => { visible = en.isIntersecting; run(); });
    io.observe(el);
    el.addEventListener("pointerenter", enter);
    el.addEventListener("focusin", stop);
    run();
    return () => {
      window.clearInterval(timer);
      io.disconnect();
      el.removeEventListener("pointerenter", enter);
      el.removeEventListener("focusin", stop);
    };
  }, []);

  const toggle = (i: number) => {
    live.current = false;
    setOn((cur) => cur.map((v, k) => (k === i ? !v : v)));
  };

  return (
    <section ref={root} className="bg-white px-5 py-16 text-slate-900 sm:py-20">
      <style>{css}</style>
      <div className="mx-auto max-w-5xl">
        <p className="text-xs font-semibold uppercase tracking-[0.25em] text-slate-500">Larkspur House</p>
        <div className="mt-3 flex flex-wrap items-end justify-between gap-4">
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">Two rooms, two outlooks</h2>
          <p className="max-w-sm text-base text-slate-600">Hover a room, or press its view button, to see what lies past the window.</p>
        </div>
        <div className="mt-10 grid gap-8 md:grid-cols-2">
          {rooms.map((r, i) => (
            <article key={r.name} className="w8rom-card" data-on={on[i] ? "" : undefined}>
              <div className="w8rom-frame">
                <img className="w8rom-img w8rom-a" src={r.a.src} alt={r.a.alt} width={800} height={600} loading="lazy" decoding="async" />
                <img className="w8rom-img w8rom-b" src={r.b.src} alt={r.b.alt} width={800} height={600} loading="lazy" decoding="async" />
                <span className="w8rom-chip" aria-hidden="true"><span>{r.a.label}</span><span>{r.b.label}</span></span>
              </div>
              <div className="mt-5 flex items-start justify-between gap-4">
                <div>
                  <h3 className="text-xl font-semibold">
                    <a href="#" className="w8rom-link rounded">{r.name}</a>
                  </h3>
                  <p className="mt-1 text-sm text-slate-600">{r.note}</p>
                  <p className="mt-2 text-sm font-medium text-slate-800">{r.rate}</p>
                </div>
                <button type="button" className="w8rom-btn h-10 shrink-0 rounded-full border border-slate-300 px-4 text-sm font-medium text-slate-800 hover:bg-slate-50" aria-pressed={on[i]} onClick={() => toggle(i)}>
                  {on[i] ? "Show room" : "Show view"}
                </button>
              </div>
            </article>
          ))}
        </div>
      </div>
    </section>
  );
}
```
