> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dnd Sort – Code

> Dnd Sort is a collection of components built for the purpose of solving bucketing and sorting, two very common applications of the drag-and-drop interaction.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="dndsort-playground" customHeight="600px" fullWidth>
      ```tsx lines expandable theme={null}
      import {
        DndSort,
        Flex,
        Grid,
        Text,
        type DndSortChangeEvent,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      type Items = {
        [key: string]: { id: string; label: string };
      };

      type Buckets = {
        [key: string]: (string | number)[];
      };

      function App() {
        const [mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
        const [buckets, setBuckets] = useState<Buckets>({
          "zone-1": [],
          "zone-2": [],
          other: ["A", "B", "C", "D", "E"],
        });
        const foodItems: Items = {
          A: { id: "A", label: "Apple 🍎" },
          B: { id: "B", label: "Banana 🍌" },
          C: { id: "C", label: "Carrot 🥕" },
          D: { id: "D", label: "Doughnut 🍩" },
          E: { id: "E", label: "Eggplant 🍆" },
        };

        const someStylesOfYourChoosing = {
          border: "1px solid var(--border-color-subdued)",
          borderRadius: "var(--size-2)",
          flexDirection: "column" as const,
        };

        const handleDrop = (event: DndSortChangeEvent) => {
          setMostRecentEvent(event);
          setBuckets((prevBuckets) => {
            // If the item wasn't dropped in a zone, do nothing
            if (!event.zoneId) return prevBuckets;

            // If the item was dropped in an invalid zone, do nothing
            if (!event.valid) return prevBuckets;

            // Create a copy of the current buckets
            const newBuckets = { ...prevBuckets };

            // Reorder or add the item to the target bucket
            if (event.zoneSort !== null) {
              newBuckets[event.zoneId] = event.zoneSort;
            }

            if (event.previousZoneId && event.previousZoneSort !== null) {
              // Reorder or remove the item from the previous bucket
              newBuckets[event.previousZoneId] = event.previousZoneSort;
            } else {
              // Remove the item from the "other" bucket
              newBuckets.other = newBuckets.other.filter(
                (id) => id !== event.draggableId,
              );
            }

            return newBuckets;
          });
        };

        return (
          <DndSort onDrop={handleDrop}>
            <Flex direction="column" gap="8" style={{ width: "100%" }}>
              {buckets.other.length > 0 && (
                <Grid templateColumns="repeat(5, 1fr)" gap="4">
                  {buckets.other.map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={foodItems[itemId].label}
                      id={itemId}
                    >
                      <Text>{foodItems[itemId].label}</Text>
                    </DndSort.Card>
                  ))}
                </Grid>
              )}

              <Grid templateColumns="repeat(2, 1fr)" gap="4">
                <DndSort.Zone
                  sortable
                  id="zone-1"
                  orientation="vertical"
                  sortedIds={buckets["zone-1"]}
                  label="Zone 1"
                  style={someStylesOfYourChoosing}
                  defaultDropPosition="end"
                >
                  <Text>Zone 1</Text>
                  {buckets["zone-1"].map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={foodItems[itemId].label}
                      id={itemId}
                    >
                      <Text>{foodItems[itemId].label}</Text>
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>

                <DndSort.Zone
                  sortable
                  id="zone-2"
                  label="Zone 2"
                  orientation="vertical"
                  sortedIds={buckets["zone-2"]}
                  style={someStylesOfYourChoosing}
                  defaultDropPosition="end"
                >
                  <Text>Zone 2</Text>
                  {buckets["zone-2"].map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={foodItems[itemId].label}
                      id={itemId}
                    >
                      <Text>{foodItems[itemId].label}</Text>
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>
              </Grid>
              <div>
                <Text>Drop event:</Text>
                <pre>{JSON.stringify(mostRecentEvent, null, 2)}</pre>
              </div>
            </Flex>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    ### Bucketing

    `DndSort` is a great way to create a drag-and-drop interface for moving items between different buckets.

    <LiveCode showCode example="dndsort-bucket" customHeight="600px" fullWidth>
      ```tsx lines expandable theme={null}
      import {
        DndSort,
        Flex,
        Grid,
        Text,
        type DndSortChangeEvent,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      type Buckets = {
        [key: string]: (string | number)[];
      };

      function App() {
        const [mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
        const [buckets, setBuckets] = useState<Buckets>({
          "zone-1": ["A", "B", "C"],
          "zone-2": ["D", "E"],
          "zone-3": ["F"],
        });

        const someStylesOfYourChoosing = {
          flexDirection: "column" as const,
        };

        const handleDrop = (event: DndSortChangeEvent) => {
          setMostRecentEvent(event);
          setBuckets((prevBuckets) => {
            // If the item wasn't dropped in a zone, do nothing
            if (!event.zoneId) return prevBuckets;

            // If the item was dropped in an invalid zone, do nothing
            if (!event.valid) return prevBuckets;

            // If the item was dropped in the zone it was already in, do nothing
            if (event.zoneId === event.previousZoneId) return prevBuckets;

            // Create a copy of the current buckets
            const newBuckets = { ...prevBuckets };

            // Remove the item from its previous bucket
            const prevKey = event.previousZoneId;
            if (prevKey) {
              newBuckets[prevKey] = newBuckets[prevKey].filter(
                (id) => id !== event.draggableId,
              );
            }

            // Add the item to the target bucket
            newBuckets[event.zoneId] = newBuckets[event.zoneId].concat(
              event.draggableId,
            );

            return newBuckets;
          });
        };

        return (
          <DndSort onDrop={handleDrop}>
            <Flex direction="column" gap="8" style={{ width: "100%" }}>
              <Grid templateColumns="repeat(3, 1fr)" gap="4">
                <DndSort.Zone
                  id="zone-1"
                  label="Zone 1"
                  style={someStylesOfYourChoosing}
                >
                  <Text>Zone 1</Text>
                  {buckets["zone-1"].map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>

                <DndSort.Zone
                  id="zone-2"
                  label="Zone 2"
                  style={someStylesOfYourChoosing}
                >
                  <Text>Zone 2</Text>
                  {buckets["zone-2"].map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>

                <DndSort.Zone
                  id="zone-3"
                  label="Zone 3"
                  style={someStylesOfYourChoosing}
                >
                  <Text>Zone 3</Text>
                  {buckets["zone-3"].map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>
              </Grid>
              <details>
                <summary>
                  <Text inline>Click to see the drop event</Text>
                </summary>
                <pre>{JSON.stringify(mostRecentEvent, null, 2)}</pre>
              </details>
            </Flex>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Sorting a list

    `DndSort` is also a great way to create a drag-and-drop interface for sorting a list of items.

    <LiveCode showCode example="dndsort-list" customHeight="600px" fullWidth>
      ```tsx lines expandable theme={null}
      import {
        DndSort,
        Flex,
        Text,
        type DndSortChangeEvent,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      type Items = {
        [key: string]: { name: string; type: string };
      };

      function App() {
        const [mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();

        const teams: Items = {
          A: { name: "Philadelphia Eagles", type: "nfl" },
          B: { name: "New York Giants", type: "nfl" },
          C: { name: "Dallas Cowboys", type: "nfl" },
          D: { name: "Washington Commanders", type: "nfl" },
          E: { name: "New York Jets", type: "nfl" },
          F: { name: "Miami Dolphins", type: "nfl" },
          G: { name: "Buffalo Bills", type: "nfl" },
        };

        const [order, setOrder] = useState<(string | number)[]>([
          "A",
          "B",
          "C",
          "D",
          "E",
          "F",
          "G",
        ]);

        const handleDrop = (event: DndSortChangeEvent) => {
          setMostRecentEvent(event);
          setOrder((previousOrder) => event.zoneSort || previousOrder);
        };

        return (
          <DndSort onDrop={handleDrop}>
            <Flex direction="column" gap="8" style={{ width: "100%" }}>
              <DndSort.Zone
                sortable
                id="zone-1"
                orientation="vertical"
                sortedIds={order}
                label="Zone 1"
                defaultDropPosition="end"
              >
                {order.map((itemId) => (
                  <DndSort.Card key={itemId} label={teams[itemId].name} id={itemId}>
                    {teams[itemId].name}
                  </DndSort.Card>
                ))}
                <Text size="small">
                  {`Sorry if your team wasn't listed, it's just an example :D`}
                </Text>
              </DndSort.Zone>
              <details>
                <summary>
                  <Text inline>Click to see the drop event</Text>
                </summary>
                <pre>{JSON.stringify(mostRecentEvent, null, 2)}</pre>
              </details>
            </Flex>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Bucket and sort

    `DndSort` can also be used to create a drag-and-drop interface for moving items between different buckets and sorting them within those buckets.

    Note this example also demonstrates cards initially beginning outside of the buckets.

    <LiveCode showCode example="dndsort-bucket-list" customHeight="600px" fullWidth>
      ```tsx lines expandable theme={null}
      import {
        DndSort,
        Flex,
        Grid,
        Text,
        type DndSortChangeEvent,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      type Buckets = {
        [key: string]: (string | number)[];
      };

      function App() {
        const [mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
        const [buckets, setBuckets] = useState<Buckets>({
          "zone-1": [],
          "zone-2": [],
          other: ["A", "B", "C", "D", "E"],
        });

        const someStylesOfYourChoosing = {
          border: "1px solid var(--border-color-subdued)",
          borderRadius: "var(--size-2)",
          flexDirection: "column" as const,
        };

        const handleDrop = (event: DndSortChangeEvent) => {
          setMostRecentEvent(event);
          setBuckets((prevBuckets) => {
            // If the item wasn't dropped in a zone, do nothing
            if (!event.zoneId) return prevBuckets;

            // If the item was dropped in an invalid zone, do nothing
            if (!event.valid) return prevBuckets;

            // Create a copy of the current buckets
            const newBuckets = { ...prevBuckets };

            // Reorder or add the item to the target bucket
            if (event.zoneSort !== null) {
              newBuckets[event.zoneId] = event.zoneSort;
            }

            if (event.previousZoneId && event.previousZoneSort !== null) {
              // Reorder or remove the item from the previous bucket
              newBuckets[event.previousZoneId] = event.previousZoneSort;
            } else {
              // Remove the item from the "other" bucket
              newBuckets.other = newBuckets.other.filter(
                (id) => id !== event.draggableId,
              );
            }

            return newBuckets;
          });
        };

        return (
          <DndSort onDrop={handleDrop}>
            <Flex direction="column" gap="8" style={{ width: "100%" }}>
              {buckets.other.length > 0 && (
                <Grid templateColumns="repeat(5, 1fr)" gap="4">
                  {buckets.other.map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </Grid>
              )}

              <Grid templateColumns="repeat(2, 1fr)" gap="4">
                <DndSort.Zone
                  sortable
                  id="zone-1"
                  orientation="vertical"
                  sortedIds={buckets["zone-1"]}
                  label="Zone 1"
                  style={someStylesOfYourChoosing}
                  defaultDropPosition="end"
                >
                  <Text>Zone 1</Text>
                  {buckets["zone-1"].map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>

                <DndSort.Zone
                  sortable
                  id="zone-2"
                  label="Zone 2"
                  orientation="vertical"
                  sortedIds={buckets["zone-2"]}
                  style={someStylesOfYourChoosing}
                  defaultDropPosition="end"
                >
                  <Text>Zone 2</Text>
                  {buckets["zone-2"].map((itemId) => (
                    <DndSort.Card key={itemId} label={`Item ${itemId}`} id={itemId}>
                      Item {itemId}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>
              </Grid>
              <details>
                <summary>
                  <Text inline>Click to see the drop event</Text>
                </summary>
                <pre>{JSON.stringify(mostRecentEvent, null, 2)}</pre>
              </details>
            </Flex>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Validation

    `DndSort` provides ways to validate the drag-and-drop interaction. This is useful for preventing invalid interactions, such as moving an item to a bucket that does not accept it.

    #### Basic Validation

    The simplest way to validate is to use the `type` prop on the `DndSort.Card` component in conjunction with `acceptedTypes` prop on the `DndSort.Zone` component.

    ```tsx theme={null}
    <DndSort.Zone acceptedTypes={["fruit"]}>
      <DndSort.Card type="fruit">🍎 Apple</DndSort.Card>
    </DndSort.Zone>
    ```

    <LiveCode showCode example="dndsort-acceptedtypes" customHeight="600px" fullWidth>
      ```tsx lines expandable theme={null}
      import {
        DndSort,
        Flex,
        Grid,
        Text,
        type DndSortChangeEvent,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      type Items = {
        [key: string | number]: { name: string; type: string };
      };

      type Buckets = {
        [key: string]: (string | number)[];
      };

      function App() {
        const [mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
        const [buckets, setBuckets] = useState<Buckets>({
          start: ["A", "B", "C", 1, 2, 3],
          "zone-1": [],
          "zone-2": [],
        });

        const handleDrop = (event: DndSortChangeEvent) => {
          setMostRecentEvent(event);
          setBuckets((prevBuckets) => {
            // If the item wasn't dropped in a zone, do nothing
            if (!event.zoneId) return prevBuckets;

            // If the item was dropped in an invalid zone, do nothing
            if (!event.valid) return prevBuckets;

            // If the item was dropped in the zone it was already in, do nothing
            if (event.zoneId === event.previousZoneId) return prevBuckets;

            // Create a copy of the current buckets
            const newBuckets = { ...prevBuckets };

            // Remove the item from its previous bucket
            const prevKey = event.previousZoneId || "start";
            newBuckets[prevKey] = newBuckets[prevKey].filter(
              (id) => id !== event.draggableId,
            );

            // Add the item to the target bucket
            newBuckets[event.zoneId] = newBuckets[event.zoneId].concat(
              event.draggableId,
            );

            return newBuckets;
          });
        };

        const options: Items = {
          A: { name: "🍓 Strawberry", type: "fruits" },
          B: { name: "🍉 Watermelon", type: "fruits" },
          C: { name: "🥭 Mango", type: "fruits" },
          1: { name: "🥦 Broccoli", type: "vegetables" },
          2: { name: "🥕 Carrot", type: "vegetables" },
          3: { name: "🌽 Corn", type: "vegetables" },
        };

        return (
          <DndSort onDrop={handleDrop}>
            <Flex direction="column" gap="8" style={{ width: "100%" }}>
              <Grid templateColumns={"repeat(3, 1fr)"} gap="8">
                <DndSort.Zone
                  id="start"
                  acceptedTypes={["vegetables", "fruits"]}
                  label="Produce basket"
                  orientation="vertical"
                >
                  {buckets["start"].map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={options[itemId].name}
                      id={itemId}
                      type={options[itemId].type}
                    >
                      {options[itemId].name}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>
                <DndSort.Zone
                  id="zone-1"
                  acceptedTypes={["fruits"]}
                  label="Fruit basket"
                  orientation="vertical"
                >
                  <Text>This zone only accepts fruits.</Text>
                  {buckets["zone-1"].map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={options[itemId].name}
                      id={itemId}
                      type={options[itemId].type}
                    >
                      {options[itemId].name}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>

                <DndSort.Zone
                  id="zone-2"
                  acceptedTypes={["vegetables"]}
                  label="Vegetable basket"
                  orientation="vertical"
                >
                  <Text>This zone only accepts vegetables</Text>
                  {buckets["zone-2"].map((itemId) => (
                    <DndSort.Card
                      key={itemId}
                      label={options[itemId].name}
                      id={itemId}
                      type={options[itemId].type}
                    >
                      {options[itemId].name}
                    </DndSort.Card>
                  ))}
                </DndSort.Zone>
              </Grid>
              <details>
                <summary>
                  <Text inline>Click to see the drop event</Text>
                </summary>
                <pre>{JSON.stringify(mostRecentEvent, null, 2)}</pre>
              </details>
            </Flex>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    #### Advanced Validation

    A more advanced way to validate is to use the `validator` prop on the `DndSort.Zone` component. This prop takes a function which receives the item `id`. The function should return a boolean indicating whether the drop is valid. Here are some examples of how to use the `validator` prop:

    ```tsx theme={null}
    // Limit the zone to only accept items with ids in an allowed list
    const validator = (id) => validIds.includes(id);
    <DndSort.Zone validator={validator}></DndSort.Zone>;

    // Limit the based on a property of the item
    const validator = (id) => people[id].isFriendly;
    <DndSort.Zone validator={validator}></DndSort.Zone>;

    // Limit the number of items in a zone to 5
    const validator = () => zoneItems.length <= 5;
    <DndSort.Zone validator={validator}></DndSort.Zone>;
    ```

    ### Customization

    `DndSort` provides a number of ways to customize the drag-and-drop interaction. This includes customizing the appearance of the items and zones.

    #### Advanced zone rendering

    The `advancedRenderer` prop of `DndSort.Zone` is an alternative to `children`, allowing you to customize the appearance of the drop zone based upon its internal state.This allows you to show different content when a drag is happening, when an item is over the drop zone, or if the dragged item is valid/invalid for the drop zone.

    #### Custom card preview rendering

    The `previewRenderer` prop of `DndSort.Card` offers a means to customize the appearance of the card when it is being dragged. This is useful for showing a different appearance for the card, such as a simpler version of a complex card.

    ### Interactive Children

    By default, the `DndSort.Card` places a button around the entire card. This is optimal for accessibility. But if you want to place interactive elements inside your card, it harms accessibility. To resolve this issue, set `dragOnlyWithHandle` on the `DndSort.Card` if your card has interactive elements.

    <LiveCode showCode example="dndsort-dragonlywithhandle" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { DndSort, Flex, Text, Button } from "@servicetitan/anvil2";

      function App() {
        return (
          <DndSort>
            <DndSort.Card id="example" label="example" dragOnlyWithHandle>
              <Flex direction="column" gap="4">
                <Text>
                  This card contains interactive elements. <br /> That means it should
                  only be draggable with the handle.
                </Text>
                <Button appearance="primary" onClick={() => alert("button clicked")}>
                  Click me
                </Button>
              </Flex>
            </DndSort.Card>
          </DndSort>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Anti-Patterns

    ### Nesting

    `DndSort` does not support nesting. i.e. you may not use a zone as an item.

    This is an intentional design decision to guarantee these components meet our accessibility and usability standards.

    If you have a use case that requires nesting, please reach out to us. We would love to hear about your use case and help you find a solution.

    ## Best Practices

    ### dragOnlyWithHandle

    ```tsx theme={null}
    <DndSort.Card dragOnlyWithHandle>
      <Button />
    </DndSort.Card>
    ```

    <Check>**Do**</Check>

    Use the dragOnlyWithHandle prop if your card contains interactive elements.

    ```tsx theme={null}
    <DndSort.Card>
      <Button />
    </DndSort.Card>
    ```

    <Danger>**Don't**</Danger>

    Don't use interactive elements in cards without dragOnlyWithHandle.
  </Tab>

  <Tab title="DndSort Props">
    ```tsx theme={null}
    import { DndSort } from "@servicetitan/anvil2";

    function ExampleComponent() {
    return (

    <DndSort onDrop={(event) => console.log(event)}>
      {/* DndSort.Zone and DndSort.Card components */}
    </DndSort>
    ); }

    ```

    ## `DndSort` Props

    `DndSort` is the root component that must wrap `DndSort` sub-components. It provides the context and internal logic for the drag-and-drop interaction.

    <ParamField path="children" type="React.ReactNode">
      Various children elements including `DndSort.Zone` and `DndSort.Card`.
    </ParamField>

    <ParamField path="onDrop" type="(DndSortChangeEvent) => void">
      Callback function fired when an item is dropped.
    </ParamField>
  </Tab>

  <Tab title="DndSort.Card Props">
    ```tsx theme={null}
    import { DndSort } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <DndSort.Card
          id="card-1"
          label="Card 1"
          dragOnlyWithHandle={false}
        >
          Card content
        </DndSort.Card>
      );
    }
    ```

    ## `DndSort.Card` Props

    `DndSort.Card` is a component that represents a card that can be dragged and dropped. It uses `Card` internally and most of the props are forwarded to the `Card`.

    <ParamField path="id" type={`string | number`} required>
      The unique identifier for the card.
    </ParamField>

    <ParamField path="label" type="string" required>
      The label for the card, used for accessibility.
    </ParamField>

    <ParamField path="children" type="React.ReactNode">
      The content of the card.
    </ParamField>

    <ParamField path="dragOnlyWithHandle" type="boolean" default="false">
      If true, the card can only be dragged when the drag handle is used. If false,
      the card can be dragged by any part of it.
    </ParamField>

    <ParamField path="onDrop" type="(DndSortChangeEvent) => void">
      Callback function to be called when the card is dropped. Same as `onDrop` on
      the `DndSort` component.
    </ParamField>

    <ParamField path="previewRenderer" type="({ valid }) => React.ReactNode">
      Advanced. A function that returns a React node to be used as the preview
      renderer.
    </ParamField>

    <ParamField path="type" type="string">
      The type of the card, used for validation (optional).
    </ParamField>
  </Tab>

  <Tab title="DndSort.Zone Props">
    ```tsx theme={null}
    import { DndSort } from "@servicetitan/anvil2";

    function ExampleComponent() {
    return (

    <DndSort.Zone
      id="zone-1"
      label="Zone 1"
      sortable={false}
      orientation="vertical"
    >
      {/* Zone content */}
    </DndSort.Zone>
    ); }

    ```

    ## `DndSort.Zone` Props

    `DndSort.Zone` is a component that represents a drop zone where items can be dropped. It may either be sortable or non-sortable. Only the basic styling is applied to the zone (i.e. border and background during a drag-and-drop interaction). Props are forwarded to the underlying `div` element, so you can provide `styles`, `className`, and other props to customize the appearance of the zone.

    <ParamField path="id" type={`string | number`} required>
      The unique identifier for the zone.
    </ParamField>

    <ParamField path="label" type="string" required>
      The label for the zone, used for accessibility.
    </ParamField>

    <ParamField path="acceptedTypes" type="string[]">
      An array of accepted types for this zone. If not provided, all types are
      accepted. Compares against the type prop of the card.
    </ParamField>

    <ParamField path="advancedRenderer" type="({ isDragging, isOver, isValid }) => React.ReactNode">
      Advanced. A function to render the drop zone using internal state parameters.
    </ParamField>

    <ParamField path="children" type="React.ReactNode">
      The contents of the zone.
    </ParamField>

    <ParamField path="defaultDropPosition" type={`"start" | "end"`} default="end">
      The default position where items will be dropped if dropped directly onto the zone.
    </ParamField>

    <ParamField path="gap" type="GapSize" default="4">
      The gap between items in the zone. See `Flex` for more info. Important for sortable zones to maintain
      enough space for the sort line.
    </ParamField>

    <ParamField path="orientation" type={`"horizontal" | "vertical"`} default="vertical">
      The orientation of the items in the zone.
    </ParamField>

    <ParamField path="sortable" type="boolean" default="false">
      If `true`, the zone is sortable. If `false`, the zone is not sortable.
    </ParamField>

    <ParamField path="sortedIds" type="(string | number)[]" default="[]">
      Required if sortable. An array of sorted IDs for the items in the zone, must
      match render order.
    </ParamField>

    <ParamField path="validator" type="(id) => boolean">
      A function to validate the drop action. Given an item's `id`, this function
      must return `true` for valid drops, `false` for invalid drops.
    </ParamField>
  </Tab>

  <Tab title="DndSortChangeEvent">
    ## DndSortChangeEvent

    <ParamField path="draggableId" type="string | number">
      The ID of the draggable item that was dropped.
    </ParamField>

    <ParamField path="previousZoneId" type="string | number | null">
      The ID of the previous drop zone where the item was located before being
      dropped. This can be `null` if the item is coming from outside of any zone.
    </ParamField>

    <ParamField path="previousZoneSort" type={`"(string" | "number)[]" | "null"`}>
      An array representing the sort order of items in the previous drop zone before
      the drop. This can be used to update the order of items in the previous zone.
      This value is `null` if the previous drop zone (if any) was not sortable.
    </ParamField>

    <ParamField path="valid" type="boolean">
      Indicates whether the drop was valid based on the accepted types and custom
      validation.
    </ParamField>

    <ParamField path="zoneId" type="string | number | null">
      The ID of the drop zone where the item was dropped. This can be `null` if the
      item was not dropped in a zone.
    </ParamField>

    <ParamField path="zoneSort" type={`"(string" | "number)[]" | "null"`}>
      An array representing the sort order of items in the drop zone after the drop.
      This can be used to update the order of items in the zone. This value is
      `null` if the target drop zone (if any) is not sortable.
    </ParamField>
  </Tab>
</Tabs>
