Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/components/nav/animated-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const PLAY: Record<string, Play> = {
'list-ordered': wave,
'list-collapse': wave,
layers: wave,
blinds: wave,
'move-right': nudge,
'unfold-horizontal': nudge,
'circle-arrow-right': nudge,
Expand Down
118 changes: 118 additions & 0 deletions apps/docs/content/2.block/shutter-slider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Shutter Slider
description: An autoplaying feature slider whose images open through alternating slats.
icon: blinds
---

A feature slider for landing and product pages. Each new image **opens through
vertical slats** — every other slat from the top, the rest from the bottom, in
a wave from the side you're heading to — while the old picture sinks back and
dims beneath it. The **headline letters roll in like drum faces**, the copy
**rises line by line** through masks and the caption **pulls into focus** from
its centre. Every move is **mirrored when you go back**. The counters roll like
an odometer and the progress line doubles as the autoplay clock. GSAP.

::component-preview{name="shutter-slider" size="xl"}
::

## Installation

::tabs
:::tabs-item{label="CLI" icon="i-lucide-terminal"}

::install-tabs{item="shutter-slider"}
::

:::
:::tabs-item{label="Manual" icon="i-lucide-clipboard"}

::component-source{name="shutter-slider"}
::

:::
::

## Usage

::tabs
:::tabs-item{label="React" icon="i-lucide-atom"}

```tsx [app/page.tsx]
import ShutterSlider from '@/components/tweenui/shutter-slider';

export default function Page() {
return <ShutterSlider />;
}
```

:::
::

Pass your own slides. Images of any shape are cropped to the same 2:1 frame:

```tsx [app/page.tsx]
import ShutterSlider from '@/components/tweenui/shutter-slider';

export default function Page() {
return (
<ShutterSlider
aria-label="Why us"
autoplay={8}
slides={[
{
heading: 'Made to measure',
image: '/images/studio.jpg',
imageAlt: 'A sunlit design studio',
description: 'Every project starts with a conversation, not a template.',
caption: 'Designed with you',
},
{
heading: 'Built to last',
image: '/images/detail.jpg',
description: 'Materials chosen to age well and keep their character.',
caption: 'Honest materials',
},
]}
/>
);
}
```

The sample photos are from [Unsplash](https://unsplash.com/license), free to use
under the Unsplash License. The background (`#eef1f4` / `#15130f` in dark mode)
and the terracotta accent live in the `className` strings on the `section` and
the pagination row, so you can restyle them in place.

## Props

| Prop | Type | Default | Description |
| ------------ | ---------------- | -------------- | ------------------------------------------------- |
| `slides` | `ShutterSlide[]` | 3-slide sample | Slides, in order. |
| `autoplay` | `number` | `6` | Seconds each slide holds. `0` turns autoplay off. |
| `aria-label` | `string` | `'Highlights'` | Accessible name for the carousel. |

Each slide is `{ heading, image, imageAlt?, description, caption? }`. Extends
native `<section>` attributes (`className`, `style`, …).

## Behavior

- **Stable height** — all slides share one grid cell, so the section is always
as tall as its tallest slide and nothing below it jumps between slides.
- **Autoplay** pauses while the pointer is over the slider, while it is off
screen and while the tab is hidden.
- **Swipe** left or right on touch screens; **← / →** step while focus is inside.
- **Resizing** re-splits the copy lines, so each line mask always matches the
real wrapping.
- Text is split once **web fonts are ready**, so lines never break in a
fallback font.

## Accessibility

::note
The slider is a `section` with `aria-roledescription="carousel"`, and each slide
is a group labelled "1 of 3" and so on. Inactive slides are `visibility: hidden`,
so screen readers only reach the current one. The prev and next controls are
real buttons with labels; the rolling counters and the shutter slats are
`aria-hidden`. Under `prefers-reduced-motion: reduce` there is no autoplay and
no motion — the controls swap slides instantly.
::
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
12 changes: 12 additions & 0 deletions apps/docs/public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,18 @@
"type": "registry:component"
}
]
},
{
"name": "shutter-slider",
"type": "registry:block",
"title": "Shutter Slider",
"description": "An autoplaying feature slider. Each image opens through vertical slats that alternate from the top and bottom, the headline letters roll in like drum faces, copy lines rise through masks and the caption pulls into focus — all mirrored when you travel back.",
"files": [
{
"path": "components/tweenui/shutter-slider.tsx",
"type": "registry:component"
}
]
}
]
}
23 changes: 23 additions & 0 deletions apps/docs/public/r/shutter-slider.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/docs/registry/__index__.generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const demos: Record<string, ComponentType> = {
'pricing-usage-slider:default': dynamic(() => import('./demos/pricing-usage-slider')),
'grid-cascade:default': dynamic(() => import('./demos/grid-cascade')),
'cube-roll-item:default': dynamic(() => import('./demos/cube-roll-item')),
'shutter-slider:default': dynamic(() => import('./demos/shutter-slider')),
};

export function getDemo(component: string, variant: string): ComponentType | undefined {
Expand Down
1 change: 1 addition & 0 deletions apps/docs/registry/__media__.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ export const discoveredMedia: Record<string, RegistryMedia> = {
'pricing-usage-slider': {"image":"/media/blocks/pricing-usage-slider-poster.webp","video":"/media/blocks/pricing-usage-slider-preview.mp4"},
'grid-cascade': {"image":"/media/blocks/grid-cascade-poster.webp","video":"/media/blocks/grid-cascade-preview.mp4"},
'cube-roll-item': {"image":"/media/blocks/cube-roll-item-poster.webp","video":"/media/blocks/cube-roll-item-preview.mp4"},
'shutter-slider': {"image":"/media/blocks/shutter-slider-poster.webp","video":"/media/blocks/shutter-slider-preview.mp4"},
};
12 changes: 12 additions & 0 deletions apps/docs/registry/__sources__.generated.ts

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions apps/docs/registry/demos/shutter-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

import ShutterSlider from '@/registry/tweenui/shutter-slider';

export default function ShutterSliderDemo() {
return (
<div className="w-full overflow-hidden rounded-xl">
<ShutterSlider />
</div>
);
}
33 changes: 33 additions & 0 deletions apps/docs/registry/registry-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,39 @@ export default function Example() {

export default function Example() {
return <CubeRollItem />;
}`,
},
},
{
name: 'shutter-slider',
type: 'block',
title: 'Shutter Slider',
description:
'An autoplaying feature slider. Each image opens through vertical slats that alternate from the top and bottom, the headline letters roll in like drum faces, copy lines rise through masks and the caption pulls into focus — all mirrored when you travel back.',
isNew: true,
media: {},
dependencies: ['gsap', '@gsap/react'],
registryDependencies: [],
cssVars: {},
files: [
{
path: 'registry/tweenui/shutter-slider.tsx',
target: 'components/tweenui/shutter-slider.tsx',
kind: 'react',
},
],
variants: [
{
id: 'default',
label: 'Preview',
reactSource: 'registry/tweenui/shutter-slider.tsx',
},
],
usage: {
react: `import ShutterSlider from '@/components/tweenui/shutter-slider';

export default function Example() {
return <ShutterSlider />;
}`,
},
},
Expand Down
Loading
Loading