diff --git a/content/guides/tune-a-protection-profile.mdx b/content/guides/tune-a-protection-profile.mdx
index dd62c79..78ca11f 100644
--- a/content/guides/tune-a-protection-profile.mdx
+++ b/content/guides/tune-a-protection-profile.mdx
@@ -1,5 +1,8 @@
---
title: Tune a Protection Profile
+description:
+ Learn how to tune your protection profiles to better protect your application while enhancing
+ your users' experiences.
---
### Throttling Windows
diff --git a/content/index.mdx b/content/index.mdx
index 60186d1..d4685f5 100644
--- a/content/index.mdx
+++ b/content/index.mdx
@@ -4,15 +4,87 @@ description: Welcome to Duckity's documentation site!
icon: Fish
---
-Welcome to Duckity's documentation site. Here you'll find everything you need to know about
-Duckity, plus some extras to help you use Duckity more efficiently and effectively.
+Welcome to Duckity's documentation site. Here you'll find everything you need to know about Duckity,
+plus some extras to help you use Duckity more efficiently and effectively.
-Duckity is an API-protection software. Unlike CAPTCHAs, which are black boxes and depend on AI
-(known to make mistakes), Duckity relies on well-studied algorithms and even some things commonly
-used to give you full control over access to your API.
+## What Duckity Is
-Duckity is NOT a replacement for basic security practices like rate limiting, and neither it is a
-DDoS protection service. Use Cloudflare, Bunny.net, or similar for protection of that kind.
+Duckity simplifies your API's protection, makes it predictable, and increases the cost an attacker
+has to pay to abuse your system.
+
+
+ }>
+ Duckity adds an invisible-to-the-user non-parallelizable CPU cost on every protected request
+ that increases the cost of abuse.
+
+ }>
+ Keep full control over your protection's behavior by tuning parameters from the dashboard.
+ Nothing is implicit.
+
+ }>
+ Monitor the system's state at any time. Access patterns are logged for as long as you
+ configure them to audit behavior in real time.
+
+ }>
+ Duckity provides cross-platform SDKs for you to use the same protection system everywhere.
+ Get started in less than 5 minutes.
+
+
+
+By layering dynamic CPU cost on top of configurable access rules and real-time adaptive costs, users
+stop being annoyed by intrusive CAPTCHA puzzles while abusers get an increasing cost.
+
+Optimized for scale and heavy concurrent access, Duckity automatically scales with your system
+without hassle.
+
+## Benefits Over CAPTCHAs and Alternatives
+
+Duckity provides several improvements over standard CAPTCHAs and CAPTCHA alternatives:
+
+
+
+ By relying on modern verifiable delay functions, Duckity is able to provide hands-off
+ security to your system and customers without sacrificing on user experience. Abuse cost
+ scales dynamically while your users get a predictable application experience, unlike
+ alternatives where delays vary substantially depending on randomness.
+
+
+ Duckity provides multiple easy-to-use cross-platform SDKs for you to use a single
+ configuration across all your clients. From web to native desktop and mobile applications,
+ CLI applications, M2M connections, videogames, IoT, and more.
+
+
+ Configure Duckity's behavior to suit your application's precise needs. Multiple features can
+ get individual configurations to beter suit each of their access patterns and requirements.
+
+
+ Most SDKs require no more than 4 lines of code to integrate with your application. Duckity
+ provides detailed documentation for every feature to help you make the most out of it.
+
+
+
+Duckity does not try to tell humans and bots apart. Instead, it enforces a background computational
+cost that humans do not notice.
+
+| | Duckity | CAPTCHAs |
+| --------------------------- | ------- | -------- |
+| Simple | ✅ | ✅ |
+| Transparent | ✅ | ❌ |
+| Predictable behavior | ✅ | ❌ |
+| Customizable behavior | ✅ | ❌ |
+| Frictionless | ✅ | ❌ |
+| Cross-platform | ✅ | ❌ |
+| Accessible | ✅ | ❌ |
+| Tells humans and bots apart | ❌ | ❓ |
+| Cost-effective | ✅ | ❓ |
+| AI-resistant | ✅ | ❓ |
+
+## Get Started
+
+You can make a free account by heading over to the [Duckity Dashboard](//app.duckity.com/hello). To
+learn how to integrate with your stack in some minutes, head over to the Quick Start page.
+
+}>
+ Integrate Duckity in less than 5 minutes.
+
-If you're new to Duckity, head over to the [Quick Start](/quick-start). It'll guide you on how to get started and
-give you some templates to reduce the boilerplate configuration tasks.
diff --git a/content/meta.json b/content/meta.json
index ed563c9..bd291bb 100644
--- a/content/meta.json
+++ b/content/meta.json
@@ -4,12 +4,8 @@
"index",
"quick-start",
"---[BookOpenText]Guides---",
- "guides/tune-a-protection-profile",
- "guides/write-your-own-sdk",
+ "...guides",
"---[CodeXml]Integrate---",
- "sdks/javascript",
- "sdks/react",
- "sdks/python",
- "sdks/rust"
+ "...sdks"
]
}
\ No newline at end of file
diff --git a/content/quick-start.mdx b/content/quick-start.mdx
index 194f26e..b2e1cd0 100644
--- a/content/quick-start.mdx
+++ b/content/quick-start.mdx
@@ -1,6 +1,6 @@
---
title: Quick Start
-description: Learn how to use Duckity in some minutes.
+description: Integrate Duckity in less than 5 minutes.
icon: Zap
---
@@ -44,36 +44,575 @@ to integrate it into your application.
Don't worry about all the settings displayed for now, you'll learn to tune them later.
-## Install an SDK [step]
+## Install a Client SDK [step]
-To integrate your protection profile with your application, you'll need a client-side SDK and a
-server-side SDK.
+To integrate your protection profile into your application, you'll need to install a client SDK in
+your client and validate solution tokens from your server. This is very simple to do and will take
+you little to no time.
-Duckity has multiple official SDKs for you to plug and play.
+
+
+
+
+ Run the following line in your terminal to install the Duckity SDK.
-import { SiC, SiJavascript, SiPython, SiRust, SiReact } from "@icons-pack/react-simple-icons";
+ ```package-install
+ @duckity/js
+ ```
-
- }>
- Integrate Duckity into your application using the JavaScript SDK.
-
- }>
- Integrate Duckity into your React app using the React SDK.
-
- }>
- Integrate Duckity into your application using the Python SDK.
-
- }>
- Integrate Duckity into your application using the Rust SDK. Supports no-std environments.
-
-
+ Then import it in your code:
-In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in
-Rust, you can link to it through the exposed C API, and it wraps all the required encoding and
-decoding in a simple API. You may also find the following guide useful:
+ ```ts
+ import * as duckity from "@duckity/js";
+ ```
+
+
+ If you're using the SDK from a static site, import it using a CDN like
+ [esm.sh](https://esm.sh/) instead.
-import { FileCodeCorner } from "lucide-react";
+ ```html
+
+ ```
+
+
+
+
+ Once you've imported the SDK, solve a challenge like follows:
+
+ ```ts
+ async function handleSubmit(e) {
+ e.preventDefault();
+ const solution: string = await duckity.solve(PROTECTION_PROFILE_ID);
+ // Send it to your backend.
+ }
+ ```
+
+ Send that solution token to your backend server. It's usually a good idea to send it in a
+ `X-Duckity-Solution` header in your request, for example
+
+ ```ts
+ await fetch("/api/login", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "X-Duckity-Solution": solution
+ },
+ body: JSON.stringify({
+ login: username,
+ password: password,
+ }),
+ });
+ ```
+
+ You can read about advanced usage in the SDK's documentation page:
+
+ }>
+ Integrate Duckity into your application using the JavaScript SDK.
+
+
+
+ Run the following line in your terminal to install the Duckity SDK.
+
+ ```package-install
+ @duckity/react
+ ```
+
+ Then import the React hook in your code:
+
+ ```ts
+ import { useChallenge } from "@duckity/react";
+ ```
+
+ You can then use the hook in your client components:
+
+
+
+ ```ts
+ "use client";
+
+ import { useChallenge } from "@duckity/react";
+
+ function MyComponent() {
+ const duckity = useChallenge(process.env.NEXT_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+
+ ```ts
+ import { useChallenge } from "@duckity/react";
+
+ function MyComponent() {
+ const duckity = useChallenge(import.meta.env.VITE_DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+
+ ```ts
+ import { json } from "@remix-run/node"; // Or cloudflare/deno
+ import { useLoaderData } from "@remix-run/react";
+ import { useChallenge } from "@duckity/react";
+
+ export async function loader() {
+ return json({
+ ENV: {
+ DUCKITY_PROTECTION_PROFILE_ID: process.env.DUCKITY_PROTECTION_PROFILE_ID,
+ },
+ });
+ }
+
+ function MyComponent() {
+ const data = useLoaderData();
+ const duckity = useChallenge(data.ENV.DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+
+ ```ts
+ import { useChallenge } from "@duckity/react";
+
+ function MyComponent() {
+ const duckity = useChallenge(process.env.GATSBY_DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+
+ ```ts
+ import { useChallenge } from "@duckity/react";
+
+ function MyComponent() {
+ const duckity = useChallenge(process.env.EXPO_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+
+ ```ts
+ import { useChallenge } from "@duckity/react";
+
+ function MyComponent() {
+ const duckity = useChallenge(import.meta.env.PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
+ }
+ ```
+
+ This is a client component, so use it as follows when rendering it:
+
+ ```tsx
+
+ ```
+
+
+
+
+ When the component is first rendered, it'll start fetching and solving a challenge.
+
+ You can then wait for and use a solution token:
+
+ ```ts
+ async function handleSubmit(e) {
+ e.preventDefault();
+ const solution = await duckity.wait();
+ // Send the solution token to your backend.
+ }
+ ```
+
+ Send that solution token to your backend server. It's usually a good idea to send it in a
+ `X-Duckity-Solution` header in your request, for example
+
+ ```ts
+ await fetch("/api/login", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "X-Duckity-Solution": solution
+ },
+ body: JSON.stringify({
+ login: username,
+ password: password,
+ }),
+ });
+ ```
+
+ You can read about advanced usage in the SDK's documentation page:
+
+ }>
+ Integrate Duckity into your application using the React SDK.
+
+
+
+ Install the SDK from PyPI using your favorite package manager:
+
+ {/* prettier-ignore */}
+
+
+ ```sh
+ pip install duckity
+ ```
+
+
+ ```sh
+ uv add duckity
+ ```
+
+
+ ```sh
+ poetry add duckity
+ ```
+
+
+
+ Solving a challenge only requires your protection profile ID.
+
+
+
+ ```py
+ import duckity
+
+ solution: str = await duckity.solve(PROTECTION_PROFILE_ID)
+ ```
+
+ The CPU-intensive part of solving the challenge is done in a
+ `ProcessPoolExecutor()`. Neither the GIL nor the async event loop will be blocked.
+
+
+ ```py
+ import asyncio
+
+ import duckity
+
+ solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
+ ```
+
+ The CPU-intensive part of solving the challenge is done in a
+ `ProcessPoolExecutor()`. The GIL will not be blocked.
+
+
+
+ Once you have a solution token, send it to your backend for validation. It's usually a good
+ idea to send it in a `X-Duckity-Solution` request header, like follows:
+
+ ```py
+ import requests
+
+ solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
+
+ requests.post(
+ "https://api.example.com/login",
+ headers={
+ "Accept": "application/json",
+ "X-Duckity-Solution": solution,
+ },
+ json={
+ "login": username,
+ "password": password
+ }
+ )
+ ```
+
+ You can read about advanced usage in the SDK's documentation page:
+
+ }>
+ Integrate Duckity into your application using the Python SDK.
+
+
+
+ Run the following cargo command in your terminal to add the `duckity` crate to your
+ project:
+
+ ```sh
+ cargo add duckity
+ ```
+
+ The crate depends on `tokio`, you can use `async-compat` to make it work in `smol`
+ contexts.
+
+ To get and solve a challenge, use `duckity::solve()` like follows:
+
+ ```rs
+ let solution: str = duckity::solve(PROTECTION_PROFILE_ID).await?;
+ ```
+
+ That's it! Once you have your solution token, you can send it over to your backend for
+ validation. It's usually a good idea to send it in a `X-Duckity-Solution` request header,
+ like follows:
+
+ ```rs
+ reqwest::Client::new()
+ .post("https://api.example.com/login")
+ .header("x-duckity-solution", solution)
+ .json(&Login {
+ login: username,
+ password: password,
+ })
+ .send()
+ .await?;
+ ```
+
+ You can read about advanced usage in the SDK's documentation page:
+
+ }>
+ Integrate Duckity into your application using the Rust SDK.
+
+
+
+ In case your platform is a different one, we provide a detailed step-by-step guide on how
+ to write your own client-side SDK implementation in any language, with code examples and
+ pseudocode. It's written so that anyone can implement a working client-side implementation
+ for any platform, so if you have a bit of time, you may be able to get it working in less
+ than an hour.
+
+ }>
+ Integrate Duckity into your application in any programming language and platform.
+
+
+ We constantly improve this guide with incoming feedback from developers like you. If you
+ find anything that could be improved, let us know!
+
+ Additionally, in case anything in the guide is not clear, open a support ticket in the
+ dashboard and we'll try to help you out. Note that we're not experts in every language nor
+ platform, so we likely won't be able to debug your code, rather help you understand what
+ the guide says.
+
+
+
+
+## Install a Server SDK [step]
+
+Once you have a solution token in your server, validating it is quite easy.
+
+
+
+ Run the following line in your terminal to install the Duckity SDK.
+
+ ```package-install
+ @duckity/js
+ ```
+
+ Then import it in your code:
+
+ ```ts
+ import * as duckity from "@duckity/js";
+ ```
+
+ To validate the solution token, use `duckity.validate()`.
+
+ ```ts
+ let isValid = await duckity.validate(
+ solution, // The solution token submitted by the client.
+ clientIp, // The IP address of the client that submitted the solution.
+ applicationSecret, // Your application's secret. Get it from the dashboard.
+ protectionProfileId, // The protection profile ID used to generate the challenge.
+ );
+ ```
+
+ That's it! If the solution is not valid, reject the request with an error. Otherwise,
+ proceed to process the rest of the request.
+
+
+ Install the SDK from PyPI using your favorite package manager:
+
+ {/* prettier-ignore */}
+
+
+ ```sh
+ pip install duckity
+ ```
+
+
+ ```sh
+ uv add duckity
+ ```
+
+
+ ```sh
+ poetry add duckity
+ ```
+
+
+
+ Once you have done it, validate the solution token as follows:
+
+
+
+ ```py
+ import duckity
+
+ solution: str # The solution token submitted by the client.
+ client_ip: str # The IP address of the client that submitted the solution.
+ application_secret: str # Your application's secret. Get it from the dashboard.
+ protection_profile_id: str # The protection profile ID used to generate the challenge.
+
+ is_valid: bool = await duckity.validate(
+ solution, client_ip, application_secret, protection_profile_id
+ )
+ ```
+
+
+ ```py
+ import asyncio
+
+ import duckity
+
+ solution: str # The solution token submitted by the client.
+ client_ip: str # The IP address of the client that submitted the solution.
+ application_secret: str # Your application's secret. Get it from the dashboard.
+ protection_profile_id: str # The protection profile ID used to generate the challenge.
+
+ is_valid: bool = asyncio.run(
+ duckity.validate(solution, client_ip, application_secret, protection_profile_id)
+ )
+ ```
+
+
+
+
+ That's it! If the solution token is valid, you can proceed to process your request. If it's
+ not, return an error to the client and do not further process the request.
+
+
+ Run the following cargo command in your terminal to add the `duckity` crate to your
+ project:
+
+ ```sh
+ cargo add duckity
+ ```
+
+ Once you have it installed, you can validate solution tokens as follows:
+
+ ```rs
+ let is_valid: bool = duckity::validate(
+ solution, // The solution token submitted by the client.
+ client_ip, // The IP address of the client that submitted the solution.
+ APPLICATION_SECRET, // Your application's secret. Get it from the dashboard.
+ PROTECTION_PROFILE_ID // The protection profile ID used to generate the challenge.
+ )
+ .await?;
+ ```
+
+ That's it! If the solution token is valid, you can proceed to process your request. If it's
+ not, return an error to the client and do not further process the request.
+
+
+ In case you cannot or do not want to install an SDK, you can validate solution tokens by
+ hand by making the HTTP request by hand instead.
+
+ ```http
+ POST /d1/challenges/{protection_profile_id}/validate HTTP/1.1
+ Host: api.duckity.com
+ Accept: application/json
+ Content-Type: application/json
+ Authorization: Bearer {application_secret}
+
+ {
+ "solution": "",
+ "ip": ""
+ }
+ ```
+
+ Validation responses are simple:
+
+
+
+ ```http
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+
+ {
+ "is_valid": true // or false
+ }
+ ```
+
+
+ ```http
+ HTTP/1.1 401 Unauthorized
+ Content-Type: application/json
+
+ {
+ "title": "Invalid Application Secret",
+ "message": "The application secret you provided was not valid."
+ }
+ ```
+
+
+ ```http
+ HTTP/1.1 404 Not Found
+ Content-Type: application/json
+
+ {
+ "title": "Protection Profile Not Found",
+ "message": "The protection profile ID you specified was not found."
+ }
+ ```
+
+
+ ```http
+ HTTP/1.1 422 Unprocessable Entity
+ Content-Type: application/json
+
+ {
+ "title": "Invalid Request Body",
+ "message": "Are you sure the JSON body you sent in the request was valid?"
+ }
+ ```
+
+
+ ```http
+ HTTP/1.1 429 Too Many Requests
+ Content-Type: application/json
+
+ {
+ "title": "Too Many Requests",
+ "message": "Wohoah, you're going too fast, buddy. Slow down a bit."
+ }
+ ```
+
+
+ ```http
+ HTTP/1.1 500 Internal Server Error
+ Content-Type: application/json
+
+ {
+ "title": "Internal Server Error",
+ "message": "The server crashed. Please try again."
+ }
+ ```
+
+
+
+ That's it! If the solution token is valid, you can proceed to process your request. If it's
+ not, return an error to the client and do not further process the request.
+
+ Note that status codes are related to server actions, not to client actions. An invalid
+ solution token submitted by the client will return `200` because the token was validated
+ successfully (even if the result was that it was invalid), while a wrong application secret
+ or an incorrect protection profile ID will return `4xx` status codes.
+
+
+
+
+## Next Steps
+
+Once you have successfully integrated duckity into your application, you're done with the code part.
+The behavior of challenges issued is fully customizable from the Duckity dashboard. Almost every
+configuration can be easily understood by their dashboard description. However, to learn how to
+combine settings to produce a more powerful setup, check out the following guide. It goes
+configuration by configuration to teach you how to configure the parameters you need to suit your
+application's needs.
+
+}
+>
+ Learn how to tune your protection profiles to better protect your application while enhancing
+ your users' experiences.
diff --git a/next.config.mjs b/next.config.mjs
index 0682f30..ef0f632 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -13,6 +13,16 @@ const config = {
images: {
unoptimized: true,
},
+ async rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: "/:path*.md",
+ destination: "/llms.mdx/docs/:path*",
+ },
+ ],
+ };
+ },
};
export default withMDX(config);
diff --git a/src/components/accordion.tsx b/src/components/accordion.tsx
new file mode 100644
index 0000000..e9d172b
--- /dev/null
+++ b/src/components/accordion.tsx
@@ -0,0 +1,105 @@
+'use client';
+
+import { Check, LinkIcon } from 'lucide-react';
+import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
+import { cn } from '../lib/cn';
+import { useCopyButton } from '@fumadocs/base-ui/utils/use-copy-button';
+import { buttonVariants } from './ui/button';
+import { mergeRefs } from '../lib/merge-refs';
+import { useTranslations } from '@fuma-translate/react';
+import {
+ Accordion as Root,
+ AccordionContent,
+ AccordionHeader,
+ AccordionItem,
+ AccordionTrigger,
+} from './ui/accordion';
+
+export function Accordions({
+ ref,
+ className,
+ defaultValue,
+ ...props
+}: ComponentProps) {
+ const rootRef = useRef(null);
+ const composedRef = mergeRefs(ref, rootRef);
+ const [value, setValue] = useState(defaultValue ?? []);
+
+ useEffect(() => {
+ const id = window.location.hash.substring(1);
+ const element = rootRef.current;
+ if (!element || id.length === 0) return;
+
+ const selected = document.getElementById(id);
+ if (!selected || !element.contains(selected)) return;
+ const value = selected.getAttribute('data-accordion-value');
+
+ if (value) setValue((prev) => [value, ...prev]);
+ }, []);
+
+ return (
+
+ cn(
+ 'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ />
+ );
+}
+
+export function Accordion({
+ title,
+ id,
+ value = String(title),
+ children,
+ ...props
+}: Omit, 'value' | 'title'> & {
+ title: string | ReactNode;
+ value?: string;
+}) {
+ return (
+
+
+ {title}
+ {id ? : null}
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function CopyButton({ id }: { id: string }) {
+ const t = useTranslations({ note: 'accordion' });
+ const [checked, onClick] = useCopyButton(() => {
+ const url = new URL(window.location.href);
+ url.hash = id;
+
+ return navigator.clipboard.writeText(url.toString());
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx
index e0d54c4..3f3c67d 100644
--- a/src/components/mdx.tsx
+++ b/src/components/mdx.tsx
@@ -1,12 +1,31 @@
import defaultMdxComponents from "fumadocs-ui/mdx";
-import * as TabsComponents from 'fumadocs-ui/components/tabs';
+import * as TabsComponents from "fumadocs-ui/components/tabs";
import Image from "next/image";
-import { Mermaid } from '@/components/mermaid';
+import { Mermaid } from "@/components/mermaid";
import type { MDXComponents } from "mdx/types";
-import * as Twoslash from 'fumadocs-twoslash/ui';
-import { TypeTable } from 'fumadocs-ui/components/type-table';
+import * as Twoslash from "fumadocs-twoslash/ui";
+import { TypeTable } from "fumadocs-ui/components/type-table";
+import * as reactSimpleIcons from "@icons-pack/react-simple-icons";
+import { icons as reactLucideIcons } from "lucide-react";
+import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
export function getMDXComponents(components?: MDXComponents) {
+ let simpleIcons: { [key: string]: any } = {};
+ for (const key in reactSimpleIcons) {
+ if (!key.endsWith("Hex")) {
+ let newKey = "Si" + key.substring(2);
+ simpleIcons[newKey] = (reactSimpleIcons as { [key: string]: any })[key];
+ }
+ }
+
+ let lucideIcons: { [key: string]: any } = {};
+ for (const key in reactLucideIcons) {
+ if (!key.endsWith("Icon")) {
+ let newKey = "Li" + key;
+ lucideIcons[newKey] = (reactLucideIcons as { [key: string]: any })[key].render;
+ }
+ }
+
return {
...defaultMdxComponents,
img: (props) => ,
@@ -14,6 +33,10 @@ export function getMDXComponents(components?: MDXComponents) {
...Twoslash,
Mermaid,
TypeTable,
+ ...simpleIcons,
+ ...lucideIcons,
+ Accordion,
+ Accordions,
...components,
} satisfies MDXComponents;
}
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..d6d3865
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -0,0 +1,85 @@
+'use client';
+
+import { Accordion as Primitive } from '@base-ui/react/accordion';
+import { ChevronRight } from 'lucide-react';
+import { type ComponentProps } from 'react';
+import { cn } from '../../lib/cn';
+
+export function Accordion({ className, ...props }: ComponentProps) {
+ return (
+
+ cn(
+ 'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ />
+ );
+}
+
+export function AccordionItem({ children, ...props }: ComponentProps) {
+ return {children};
+}
+
+export function AccordionHeader({
+ className,
+ children,
+ ...props
+}: ComponentProps) {
+ return (
+
+ cn(
+ 'scroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ >
+ {children}
+
+ );
+}
+
+export function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: ComponentProps) {
+ return (
+
+ cn(
+ 'group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ >
+
+ {children}
+
+ );
+}
+
+export function AccordionContent({
+ className,
+ children,
+ ...props
+}: ComponentProps) {
+ return (
+
+ cn(
+ 'h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-ending-style:h-0 data-starting-style:h-0',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ >
+ {children}
+
+ );
+}