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
75 changes: 75 additions & 0 deletions apps/docs/content/1.component/arrow-tile-button.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Arrow Tile Button
description: A label slab paired with a detached arrow tile, in a springy tilt and a sliding swap variant.
icon: square-arrow-right
---

A label slab sits beside its own **square arrow tile**, and the pair animates
two ways. In `tilt`, the tile **knocks the word**: it leans into the slab, the
slab is shoved back and springs into place, and the letters **topple in a wave**
that travels away from the tile before bouncing upright. In `swap`, the tile **takes the lead**: it rolls across to the front of the
label, its arrow spinning a full turn, while the label slides aside and its
letters **lift in a wake** behind it. Pressing either variant squashes the whole
button. It is a real `<button>`, so `onClick`, `type`,
`disabled`, and refs all work. Pure CSS — no GSAP dependency.

::component-preview{name="arrow-tile-button"}
::

## Installation

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

::install-tabs{item="arrow-tile-button"}
::

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

::component-source{name="arrow-tile-button"}
::

:::
::

## Usage

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

```tsx [app/page.tsx]
import ArrowTileButton from '@/components/tweenui/arrow-tile-button';

export default function Page() {
return (
<ArrowTileButton variant="swap" onClick={() => console.log('clicked')}>
Book a call
</ArrowTileButton>
);
}
```

:::
::

## Props

Extends every native `<button>` attribute (`onClick`, `type`, `disabled`, `ref`, …).

| Prop | Type | Default | Description |
| ----------- | --------------------------------- | ---------- | -------------------------------------------------------------------------------------------------- |
| `children` | `string` | — | Button label (a string — split into letters for the wave). |
| `variant` | `'tilt' \| 'swap'` | `'tilt'` | `tilt` knocks the letters over from the tile, `swap` rolls the tile to the front of the lime slab. |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type. |
| `className` | `string` | — | Merged with the base styles. |

## Accessibility

::note
It renders a native `<button>`; the visible label supplies its accessible name,
while the split letters and the arrow tile are
`aria-hidden`. Hover and
keyboard focus both trigger the motion, and a focus ring is included. Under
`prefers-reduced-motion: reduce` the transitions are switched off.
::
72 changes: 72 additions & 0 deletions apps/docs/content/1.component/focus-frame-button.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Focus Frame Button
description: A framed button whose hairline frame locks onto it like a viewfinder on hover.
icon: scan
---

A lime button sits inside a hairline frame. Hover or focus it and the frame
**locks on like a viewfinder**: each side of the hairline splits and folds back
into its corner, the four corner brackets **snap shut** around the button with
a springy overshoot. Meanwhile
the label **rolls up** to a fresh copy and a new return arrow **slides in**,
pushing the old one out. It is a real
`<button>`, so `onClick`, `type`, `disabled`, and refs all work. Pure CSS — no
GSAP dependency.

::component-preview{name="focus-frame-button"}
::

## Installation

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

::install-tabs{item="focus-frame-button"}
::

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

::component-source{name="focus-frame-button"}
::

:::
::

## Usage

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

```tsx [app/page.tsx]
import FocusFrameButton from '@/components/tweenui/focus-frame-button';

export default function Page() {
return (
<FocusFrameButton onClick={() => console.log('clicked')}>Talk to the team</FocusFrameButton>
);
}
```

:::
::

## Props

Extends every native `<button>` attribute (`onClick`, `type`, `disabled`, `ref`, …).

| Prop | Type | Default | Description |
| ----------- | --------------------------------- | ---------- | -------------------------------------------------------- |
| `children` | `string` | — | Button label (a string — it is duplicated for the roll). |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type. |
| `className` | `string` | — | Merged with the base styles. |

## Accessibility

::note
It renders a native `<button>`; the visible label supplies its accessible name,
while the rolled label copy, the frame and the arrows are `aria-hidden`.
Keyboard focus triggers the same lock-on as hover, so the closing corners double
as the focus indicator. Under `prefers-reduced-motion: reduce` the transitions
are switched off.
::
69 changes: 69 additions & 0 deletions apps/docs/content/1.component/slant-morph-button.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Slant Morph Button
description: A slanted label and arrow tile whose diagonal seam smoothly morphs from / to \ on hover, like a hinge.
icon: square-slash
---

A monospace label slab meets a slanted arrow tile along a diagonal seam. Hover
or focus it and the seam **swings like a hinge**: both edges are genuinely
reshaped with GSAP MorphSVG, easing the diagonal from `/` to `\` in one smooth
stroke, rather than crossfading two pictures. At the same time the colors
trade places and a fresh arrow slides through. It is a real `<button>`, so
`onClick`, `type`, `disabled`, and refs all work.

::component-preview{name="slant-morph-button"}
::

## Installation

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

::install-tabs{item="slant-morph-button"}
::

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

::component-source{name="slant-morph-button"}
::

:::
::

## Usage

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

```tsx [app/page.tsx]
import SlantMorphButton from '@/components/tweenui/slant-morph-button';

export default function Page() {
return (
<SlantMorphButton onClick={() => console.log('clicked')}>Discover the library</SlantMorphButton>
);
}
```

:::
::

## Props

Extends every native `<button>` attribute (`onClick`, `type`, `disabled`, `ref`, …).

| Prop | Type | Default | Description |
| ----------- | --------------------------------- | ---------- | ---------------------------- |
| `children` | `string` | — | Button label. |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type. |
| `className` | `string` | — | Merged with the base styles. |

## Accessibility

::note
It renders a native `<button>`; the visible label supplies its accessible name
and the seam, tile and arrows are `aria-hidden`. Hover and keyboard focus both
trigger the morph. Under `prefers-reduced-motion: reduce`, or on touch screens
without hover, the shapes stay put and only the colors change.
::
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.
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.
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.
17 changes: 17 additions & 0 deletions apps/docs/public/r/arrow-tile-button-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "arrow-tile-button-demo",
"type": "registry:example",
"title": "Arrow Tile Button Demo",
"description": "Usage examples for Arrow Tile Button.",
"registryDependencies": [
"https://tween-ui.vercel.app/r/arrow-tile-button.json"
],
"files": [
{
"path": "examples/arrow-tile-button-demo.tsx",
"type": "registry:example",
"content": "import ArrowTileButton from '@/components/tweenui/arrow-tile-button';\n\nexport default function Page() {\n return (\n <ArrowTileButton variant=\"swap\" onClick={() => console.log('clicked')}>\n Book a call\n </ArrowTileButton>\n );\n}"
}
]
}
21 changes: 21 additions & 0 deletions apps/docs/public/r/arrow-tile-button.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "arrow-tile-button",
"type": "registry:ui",
"title": "Arrow Tile Button",
"description": "A label slab paired with a detached arrow tile: the tilt variant has the tile knock the slab so its letters topple in a wave away from it and spring upright, and the swap variant rolls the tile to the front as the letters lift in its wake.",
"dependencies": [],
"registryDependencies": [],
"cssVars": {
"theme": {}
},
"files": [
{
"path": "components/tweenui/arrow-tile-button.tsx",
"type": "registry:component",
"target": "components/tweenui/arrow-tile-button.tsx",
"content": "import type { ComponentPropsWithRef, CSSProperties } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface ArrowTileButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n variant?: 'tilt' | 'swap';\n}\n\nfunction Arrow({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"square\"\n aria-hidden=\"true\"\n className={cn('size-4 flex-none', className)}\n >\n <path d=\"M2.5 8h10M8.5 3.5 13 8l-4.5 4.5\" />\n </svg>\n );\n}\n\ninterface Wave {\n leadMs: number;\n spreadMs: number;\n}\n\nfunction waveDelay(index: number, count: number, { leadMs, spreadMs }: Wave): CSSProperties {\n const step = count > 1 ? spreadMs / (count - 1) : 0;\n const style: CSSProperties & { '--hop-delay': string } = {\n '--hop-delay': `${leadMs + Math.round((count - 1 - index) * step)}ms`,\n };\n return style;\n}\n\nconst toppleChar =\n 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_45%] group-[:is(:hover,:focus-visible)]:[rotate:-14deg] group-[:is(:hover,:focus-visible)]:[scale:1_0.35] group-[:is(:hover,:focus-visible)]:[transform:scale(1,2.8571)_rotate(14deg)_translateY(-45%)] group-[:is(:hover,:focus-visible)]:[transition:translate_145ms_var(--dip)_var(--hop-delay),rotate_145ms_var(--dip)_var(--hop-delay),scale_145ms_var(--dip)_var(--hop-delay),transform_580ms_var(--spring)_calc(var(--hop-delay)+145ms)] motion-reduce:transition-none';\n\nconst liftChar =\n 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_-40%] group-[:is(:hover,:focus-visible)]:[rotate:9deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(-9deg)_translateY(40%)] group-[:is(:hover,:focus-visible)]:[transition:translate_160ms_var(--lift)_var(--hop-delay),rotate_160ms_var(--lift)_var(--hop-delay),transform_560ms_var(--spring)_calc(var(--hop-delay)+160ms)] motion-reduce:transition-none';\n\nfunction WaveLabel({\n children,\n charClassName,\n wave,\n}: {\n children: string;\n charClassName: string;\n wave: Wave;\n}) {\n const chars = Array.from(children);\n return (\n <>\n <span className=\"sr-only\">{children}</span>\n <span aria-hidden=\"true\">\n {chars.map((char, index) => (\n <span key={index} className={charClassName} style={waveDelay(index, chars.length, wave)}>\n {char}\n </span>\n ))}\n </span>\n </>\n );\n}\n\nconst TOPPLE_WAVE: Wave = { leadMs: 140, spreadMs: 225 };\nconst LIFT_WAVE: Wave = { leadMs: 40, spreadMs: 340 };\n\nconst knockRelease =\n 'grid [transition:translate_150ms_var(--soft),rotate_150ms_var(--soft),scale_150ms_var(--soft),transform_150ms_var(--soft)] motion-reduce:transition-none';\n\nfunction TiltContent({ children }: { children: string }) {\n return (\n <>\n <span\n className={cn(\n knockRelease,\n 'origin-left group-[:is(:hover,:focus-visible)]:[translate:-0.3em_0] group-[:is(:hover,:focus-visible)]:[scale:0.95_1.04] group-[:is(:hover,:focus-visible)]:[transform:scale(1.0526,0.9615)_translateX(0.3em)] group-[:is(:hover,:focus-visible)]:[transition:translate_110ms_var(--dip)_60ms,scale_110ms_var(--dip)_60ms,transform_420ms_170ms_var(--bounce)]'\n )}\n >\n <span aria-hidden=\"true\" className=\"col-start-1 row-start-1 rounded-md bg-[#045f64]\" />\n <span className=\"relative col-start-1 row-start-1 flex items-center px-5\">\n <WaveLabel charClassName={toppleChar} wave={TOPPLE_WAVE}>\n {children}\n </WaveLabel>\n </span>\n </span>\n <span\n aria-hidden=\"true\"\n className={cn(\n knockRelease,\n '-ml-px size-11 origin-bottom-left group-[:is(:hover,:focus-visible)]:[translate:-0.2em_0] group-[:is(:hover,:focus-visible)]:[rotate:-12deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(12deg)_translateX(0.2em)] group-[:is(:hover,:focus-visible)]:[transition:translate_120ms_var(--dip),rotate_120ms_var(--dip),transform_400ms_120ms_var(--bounce)]'\n )}\n >\n <span className=\"col-start-1 row-start-1 rounded-md bg-[#045f64]\" />\n <span className=\"relative col-start-1 row-start-1 grid place-items-center\">\n <Arrow />\n </span>\n </span>\n </>\n );\n}\n\nfunction SwapContent({ children }: { children: string }) {\n return (\n <>\n <span className=\"flex items-center rounded-md bg-[#c6f56f] px-5 text-[#045f64] transition-[translate] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:translate-x-[2.875rem] motion-reduce:transition-none\">\n <WaveLabel charClassName={liftChar} wave={LIFT_WAVE}>\n {children}\n </WaveLabel>\n </span>\n <span\n aria-hidden=\"true\"\n className=\"absolute top-0 right-0 z-10 grid size-11 place-items-center rounded-md bg-[#045f64] text-[#c6f56f] transition-[right] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:right-[calc(100%-2.75rem)] motion-reduce:transition-none\"\n >\n <Arrow className=\"transition-[rotate] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:-rotate-360 motion-reduce:transition-none\" />\n </span>\n </>\n );\n}\n\nconst variants = {\n tilt: 'text-[#c6f56f] [--bounce:cubic-bezier(0.34,2.27,0.64,1)] [--dip:cubic-bezier(0.55,0.085,0.68,0.53)] [--soft:cubic-bezier(0.59,1,0.88,1.01)]',\n swap: 'pr-[2.875rem] [--circ:cubic-bezier(0.785,0.135,0.15,0.86)] [--lift:cubic-bezier(0.33,1,0.68,1)]',\n};\n\nexport default function ArrowTileButton({\n children,\n className,\n variant = 'tilt',\n type = 'button',\n ...props\n}: ArrowTileButtonProps) {\n return (\n <button\n type={type}\n className={cn(\n 'group relative inline-flex h-11 cursor-pointer items-stretch text-sm font-medium tracking-wide whitespace-nowrap uppercase transition-[scale] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] select-none [--spring:linear(0,0.406_4.2%,0.855_8.3%,1.161_12.5%,1.273_16.7%,1.234_20.8%,1.125_25%,1.017_29.2%,0.95_33.3%,0.931_37.5%,0.946_41.7%,0.975_45.8%,1_50%,1.014_54.2%,1.017_58.3%,1.012_62.5%,1.005_66.7%,0.999_70.8%,0.996_75%,0.997_83.3%,1_91.7%,1)] focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none active:[scale:0.955_0.925] disabled:pointer-events-none disabled:opacity-60 motion-reduce:transition-none dark:ring-offset-[#12161F]',\n variants[variant],\n className\n )}\n {...props}\n >\n {variant === 'swap' ? (\n <SwapContent>{children}</SwapContent>\n ) : (\n <TiltContent>{children}</TiltContent>\n )}\n </button>\n );\n}\n"
}
],
"docs": "Docs, props and examples: https://tween-ui.vercel.app/component/arrow-tile-button"
}
17 changes: 17 additions & 0 deletions apps/docs/public/r/focus-frame-button-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "focus-frame-button-demo",
"type": "registry:example",
"title": "Focus Frame Button Demo",
"description": "Usage examples for Focus Frame Button.",
"registryDependencies": [
"https://tween-ui.vercel.app/r/focus-frame-button.json"
],
"files": [
{
"path": "examples/focus-frame-button-demo.tsx",
"type": "registry:example",
"content": "import FocusFrameButton from '@/components/tweenui/focus-frame-button';\n\nexport default function Page() {\n return (\n <FocusFrameButton onClick={() => console.log('clicked')}>Talk to the team</FocusFrameButton>\n );\n}"
}
]
}
Loading
Loading